Vuex - Computed property "searchPhrase" was assigned to but it has no setter - vue.js

I have a searchbar where user can get characters filtered by his input.
//Dummy input to check if it works
<input
class="form-control"
type="text"
placeholder="Search"
v-model="searchPhrase"
/>
<Character
v-for="character in resultSearching"
:key="character.id"
:photo="character.image"
:characterID="character.id"
:name="character.name"
:gender="character.gender"
:species="character.species"
:lastEpisode="character.episode[character.episode.length - 1].episode"
:character="character"
/>
</div>
</template>
<script lang="ts">
import { Vue, Component } from "vue-property-decorator";
import Character from "./Character.vue";
import { CharactersApiI } from "#/models/models";
import { Getter } from "vuex-class";
#Component({
components: {
Character,
},
})
export default class Characters extends Vue {
#Getter("characters/getCharacters") characters!: CharactersApiI[];
#Getter("characters/getLoading") loading!: boolean;
#Getter("characters/getError") error!: boolean;
#Getter("characters/getSearchPhrase") searchPhrase!: string;
#Getter("characters/getHeaders") headers!: string[];
get resultSearching(): any {
return this.characters.filter((character) => {
return character.name
.toLowerCase()
.match(this.searchPhrase.toLowerCase());
});
}
set resultSearching(newValue: any) {
this.searchPhrase = newValue.target.value.toLowerCase();
}
I get state stored in characters.ts by #Getter from vuex-class and it gets its value with :value on input or v-model but it starts breaking instantly after I try to write something there.
Updated version due to LLai's answer(working):
//characters.ts
#Mutation
updateMessage(e: { target: {value: string}}) {
this.searchPhrase = e.target.value;
}
//characters.vue
export default class Characters extends Vue {
#Getter("characters/getCharacters") characters!: CharactersApiI[];
#Getter("characters/getSearchPhrase") searchPhrase!: string;
#Mutation("characters/updateMessage") updateMessage!: (e: {
target: { value: string };
}) => void;
get resultSearching(): CharactersApiI[] {
return this.characters.filter((character) => {
return character.name
.toLowerCase()
.match(this.searchPhrase.toLowerCase());
});
}
handleInput(e: {target: {value: string}}) {
this.updateMessage(e);
}

Related

How to use pinia store with v-for directive whilst keeping it reactive?

I have a pinia data store similar to the following code snippet that stores user information and a list of individual orders he is placing:
order.js
import { defineStore } from 'pinia'
import { reactive } from 'vue'
export const useOrderStore = defineStore('order', {
state: () => ({
username: '',
orders: reactive([
{
id: '',
item: '',
price: ''
}
])
}),
})
Also I am using the v-for directive to render the components that should display the individual orders
OrdersComp.vue
<template>
<div class="orders_container">
<div v-for="(order, index) in orders" :key="order.id">
<OrderComp />
</div>
</div>
</template>
<script>
import { storeToRefs } from 'pinia'
import { useOrderStore } from "#/store/order";
setup() {
const { orders } = storeToRefs(useOrderStore())
return { orders };
},
</script>
How can I access the store data for the individual orders in the child component OrderComp
Basically I want something like this:
OrderComp.vue
<div>
<p>{{ orders.id }}</p>
<input v-model="orders.item" />
<input v-model="orders.price" />
</div>
<script>
import { storeToRefs } from 'pinia'
import { useOrderStore } from "#/store/order";
setup() {
const { orders } = storeToRefs(useOrderStore())
return { orders };
},
</script>
and still keep its reactive state? How does the child component know which order of the orders array to modify? Can/Should I combine the pinia data store with props that pass the data from parent to child? (Though this seems somewhat wrong for me, as pinia is probably able to replace all data passing between components) And furthermore as item and price are bound to input fields, they should of course dynamically change based on a user input.
Based on Estus Flasks comments I got it working by emitting events from the child OrderComp to the parent OrdersComp and on each change it invoked a function that modified my orders array at the correct index in the datastore.
So following the example above I did something like this:
order.js
import { defineStore } from 'pinia'
import { reactive } from 'vue'
export const useOrderStore = defineStore('order', {
state: () => ({
username: '',
orders: reactive([
{
id: '',
item: '',
price: ''
}
])
}),
actions: {
modifyOrder (id, order) {
var foundIndex = this.orders.findIndex(elem => elem.id == order.id)
this.orders[foundIndex] = order
}
}
})
OrdersComp.vue
<template>
<div class="orders_container">
<div v-for="(order, index) in order_store.orders" :key="order.id">
<OrderComp #change="order_store.modifyOrder(index, order)"
v-model:itemProp="order.item"
v-model:priceProp="order.price"
/>
</div>
</div>
</template>
<script>
import { useOrderStore } from "#/store/order";
export default {
setup() {
const order_store = useOrderStore()
return { order_store };
},
}
</script>
Note: I use a wrapper function here to emit the inputs, however you can of course emit it directly e.g. via #input/#change
OrderComp.vue
<div>
<input v-model="item" />
<input v-model="price" />
</div>
<script>
import { useModelWrapper } from "#/modelWrapper";
export default {
name: "OrderComp",ยด
props: {
itemProp: { type: String, default: "" },
priceProp: { type: String, default: "" },
},
emits: [
"update:itemProp",
"update:priceProp",
],
setup(props, { emit }) {
return {
item: useModelWrapper(props, emit, "itemProp"),
price: useModelWrapper(props, emit, "priceProp"),
};
},
}
modelWrapper.js
import { computed } from "vue";
export function useModelWrapper(props, emit, name = "modelValue") {
return computed({
get: () => props[name],
set: (value) => emit(`update:${name}`, value),
});
}

Default value for props like an object didn't work

I have a very strange situation :
My HomeComponent.vue
<template>
<div class="home">
<img alt="Vue logo" src="../assets/logo.png" />
<HelloWorld :msg="msg" #update="inputUpdated" />
</div>
</template>
<script lang="ts">
import { defineComponent } from "vue";
import HelloWorld from "#/components/HelloWorld.vue"; // # is an alias to /src
import { Message } from "#/interfaces/message";
export default defineComponent({
name: "Home",
components: {
HelloWorld,
},
setup(props, { emit }) {
const msg: Message = {
year: 2020,
};
function inputUpdated(value): void {
console.log("Get Event : " + value);
}
return {
msg,
inputUpdated,
};
},
});
</script>
My HelloWorld.vue
<template>
<div class="hello">
<h1>{{ msg.title }} in the year : {{ msg.year }}</h1>
<button #click="increment">count is : {{ count }}</button>
<div>{{ pow(2, 3) }}</div>
<p>Edit <code>component</code> to test state is {{ state.year }}</p>
</div>
</template>
<script lang="ts">
import { defineComponent, ref, reactive, PropType } from "vue";
import { State } from "#/interfaces/state";
import { Message } from "#/interfaces/message";
export default defineComponent({
name: "HelloWorld",
props: {
msg: {
type: Object as PropType<Message>,
required: true,
default: function () {
return {
title: "Arrow Function Expression",
year: 2020,
};
},
},
},
setup(props, { emit }) {
console.log(props);
const count = ref(0);
const state: State = reactive({
title: "",
year: 2020,
});
function pow(x: number, y: number): number {
return Math.pow(x, y);
}
function increment(): void {
console.log("Emit : Hello World");
emit("update", "Hello World");
count.value++;
}
return {
count,
increment,
pow,
state,
};
},
});
</script>
The interface Message :
export interface Message {
title?: string;
year?: number;
}
The problem what I have is even if I created a default for msg props didn't work, is not taken into account. I have the message in the year :. What I'm doing wrong with default values for props values ? Thx in advance and sorry for my english.
You're expecting the default prop's properties to be merged with the passed prop. That won't work and would mean the prop was modified, and props should not be modified. The default prop is used only when no prop has been passed (so you don't need the required attribute.)
You'll have to decide between splitting up the object into individual props:
props: {
title: ...,
year: ...
}
Or use a computed to merge:
setup(props) {
const msgDefault = {
title: "Arrow Function Expression",
year: 2020,
};
const merged = computed(() => {
return Object.assign({}, msgDefault, props.msg); // shallow merge
})
return { merged }
}

Vue 3 access child component from slots

I am currently working on a custom validation and would like to, if possible, access a child components and call a method in there.
Form wrapper
<template>
<form #submit.prevent="handleSubmit">
<slot></slot>
</form>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({
setup(props, { slots }) {
const validate = (): boolean => {
if (slots.default) {
slots.default().forEach((vNode) => {
if (vNode.props && vNode.props.rules) {
if (vNode.component) {
vNode.component.emit('validate');
}
}
});
}
return false;
};
const handleSubmit = (ev: any): void => {
validate();
};
return {
handleSubmit,
};
},
});
</script>
When I call slot.default() I get proper list of child components and can see their props. However, vNode.component is always null
My code is based from this example but it is for vue 2.
If someone can help me that would be great, or is this even possible to do.
I found another solution, inspired by quasar framework.
Form component provide() bind and unbind function.
bind() push validate function to an array and store in Form component.
Input component inject the bind and unbind function from parent Form component.
run bind() with self validate() function and uid
Form listen submit event from submit button.
run through all those validate() array, if no problem then emit('submit')
Form Component
import {
defineComponent,
onBeforeUnmount,
onMounted,
reactive,
toRefs,
provide
} from "vue";
export default defineComponent({
name: "Form",
emits: ["submit"],
setup(props, { emit }) {
const state = reactive({
validateComponents: []
});
provide("form", {
bind,
unbind
});
onMounted(() => {
state.form.addEventListener("submit", onSubmit);
});
onBeforeUnmount(() => {
state.form.removeEventListener("submit", onSubmit);
});
function bind(component) {
state.validateComponents.push(component);
}
function unbind(uid) {
const index = state.validateComponents.findIndex(c => c.uid === uid);
if (index > -1) {
state.validateComponents.splice(index, 1);
}
}
function validate() {
let valid = true;
for (const component of state.validateComponents) {
const result = component.validate();
if (!result) {
valid = false;
}
}
return valid;
}
function onSubmit() {
const valid = validate();
if (valid) {
emit("submit");
}
}
}
});
Input Component
import { defineComponent } from "vue";
export default defineComponent({
name: "Input",
props: {
rules: {
default: () => [],
type: Array
},
modelValue: {
default: null,
type: String
}
}
setup(props) {
const form = inject("form");
const uid = getCurrentInstance().uid;
onMounted(() => {
form.bind({ validate, uid });
});
onBeforeUnmount(() => {
form.unbind(uid);
});
function validate() {
// validate logic here
let result = true;
props.rules.forEach(rule => {
const value = rule(props.modelValue);
if(!value) result = value;
})
return result;
}
}
});
Usage
<template>
<form #submit="onSubmit">
<!-- rules function -->
<input :rules="[(v) => true]">
<button label="submit form" type="submit">
</form>
</template>
In the link you provided, Linus mentions using $on and $off to do this. These have been removed in Vue 3, but you could use the recommended mitt library.
One way would be to dispatch a submit event to the child components and have them emit a validate event when they receive a submit. But maybe you don't have access to add this to the child components?
JSFiddle Example
<div id="app">
<form-component>
<one></one>
<two></two>
<three></three>
</form-component>
</div>
const emitter = mitt();
const ChildComponent = {
setup(props, { emit }) {
emitter.on('submit', () => {
console.log('Child submit event handler!');
if (props && props.rules) {
emit('validate');
}
});
},
};
function makeChild(name) {
return {
...ChildComponent,
template: `<input value="${name}" />`,
};
}
const formComponent = {
template: `
<form #submit.prevent="handleSubmit">
<slot></slot>
<button type="submit">Submit</button>
</form>
`,
setup() {
const handleSubmit = () => emitter.emit('submit');
return { handleSubmit };
},
};
const app = Vue.createApp({
components: {
formComponent,
one: makeChild('one'),
two: makeChild('two'),
three: makeChild('three'),
}
});
app.mount('#app');

VueJS input with v-model breaks the user input [duplicate]

I have a simple input box in a Vue template and I would like to use debounce more or less like this:
<input type="text" v-model="filterKey" debounce="500">
However the debounce property has been deprecated in Vue 2. The recommendation only says: "use v-on:input + 3rd party debounce function".
How do you correctly implement it?
I've tried to implement it using lodash, v-on:input and v-model, but I am wondering if it is possible to do without the extra variable.
In template:
<input type="text" v-on:input="debounceInput" v-model="searchInput">
In script:
data: function () {
return {
searchInput: '',
filterKey: ''
}
},
methods: {
debounceInput: _.debounce(function () {
this.filterKey = this.searchInput;
}, 500)
}
The filterkey is then used later in computed props.
I am using debounce NPM package and implemented like this:
<input #input="debounceInput">
methods: {
debounceInput: debounce(function (e) {
this.$store.dispatch('updateInput', e.target.value)
}, config.debouncers.default)
}
Using lodash and the example in the question, the implementation looks like this:
<input v-on:input="debounceInput">
methods: {
debounceInput: _.debounce(function (e) {
this.filterKey = e.target.value;
}, 500)
}
Option 1: Re-usable, no deps
- Recommended if needed more than once in your project
/helpers.js
export function debounce (fn, delay) {
var timeoutID = null
return function () {
clearTimeout(timeoutID)
var args = arguments
var that = this
timeoutID = setTimeout(function () {
fn.apply(that, args)
}, delay)
}
}
Typescript?
export function debounce<T extends (...args: any[]) => void>(fn: T, delay: number): T {
let timeoutID: number | null = null;
return function (this: any, ...args: any[]) {
clearTimeout(timeoutID);
timeoutID = setTimeout(() => {
fn.apply(this, args);
}, delay);
} as T;
}
Or if using a d.ts:
declare function debounce(fn: (...args: any[]) => void, delay: number): (...args: any[]) => void;
/Component.vue
<script>
import {debounce} from './helpers'
export default {
data () {
return {
input: '',
debouncedInput: ''
}
},
watch: {
input: debounce(function (newVal) {
this.debouncedInput = newVal
}, 500)
}
}
</script>
Codepen
Option 2: In-component, also no deps
- Recommended if using once or in small project
/Component.vue
<template>
<input type="text" v-model="input" />
</template>
<script>
export default {
data: {
timeout: null,
debouncedInput: ''
},
computed: {
input: {
get() {
return this.debouncedInput
},
set(val) {
if (this.timeout) clearTimeout(this.timeout)
this.timeout = setTimeout(() => {
this.debouncedInput = val
}, 300)
}
}
}
}
</script>
Codepen
Assigning debounce in methods can be trouble. So instead of this:
// Bad
methods: {
foo: _.debounce(function(){}, 1000)
}
You may try:
// Good
created () {
this.foo = _.debounce(function(){}, 1000);
}
It becomes an issue if you have multiple instances of a component - similar to the way data should be a function that returns an object. Each instance needs its own debounce function if they are supposed to act independently.
Here's an example of the problem:
Vue.component('counter', {
template: '<div>{{ i }}</div>',
data: function(){
return { i: 0 };
},
methods: {
// DON'T DO THIS
increment: _.debounce(function(){
this.i += 1;
}, 1000)
}
});
new Vue({
el: '#app',
mounted () {
this.$refs.counter1.increment();
this.$refs.counter2.increment();
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.16/vue.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.5/lodash.min.js"></script>
<div id="app">
<div>Both should change from 0 to 1:</div>
<counter ref="counter1"></counter>
<counter ref="counter2"></counter>
</div>
Very simple without lodash
handleScroll: function() {
if (this.timeout)
clearTimeout(this.timeout);
this.timeout = setTimeout(() => {
// your action
}, 200); // delay
}
I had the same problem and here is a solution that works without plugins.
Since <input v-model="xxxx"> is exactly the same as
<input
v-bind:value="xxxx"
v-on:input="xxxx = $event.target.value"
>
(source)
I figured I could set a debounce function on the assigning of xxxx in xxxx = $event.target.value
like this
<input
v-bind:value="xxxx"
v-on:input="debounceSearch($event.target.value)"
>
methods:
debounceSearch(val){
if(search_timeout) clearTimeout(search_timeout);
var that=this;
search_timeout = setTimeout(function() {
that.xxxx = val;
}, 400);
},
If you need a very minimalistic approach to this, I made one (originally forked from vuejs-tips to also support IE) which is available here: https://www.npmjs.com/package/v-debounce
Usage:
<input v-model.lazy="term" v-debounce="delay" placeholder="Search for something" />
Then in your component:
<script>
export default {
name: 'example',
data () {
return {
delay: 1000,
term: '',
}
},
watch: {
term () {
// Do something with search term after it debounced
console.log(`Search term changed to ${this.term}`)
}
},
directives: {
debounce
}
}
</script>
Please note that I posted this answer before the accepted answer. It's not
correct. It's just a step forward from the solution in the
question. I have edited the accepted question to show both the author's implementation and the final implementation I had used.
Based on comments and the linked migration document, I've made a few changes to the code:
In template:
<input type="text" v-on:input="debounceInput" v-model="searchInput">
In script:
watch: {
searchInput: function () {
this.debounceInput();
}
},
And the method that sets the filter key stays the same:
methods: {
debounceInput: _.debounce(function () {
this.filterKey = this.searchInput;
}, 500)
}
This looks like there is one less call (just the v-model, and not the v-on:input).
In case you need to apply a dynamic delay with the lodash's debounce function:
props: {
delay: String
},
data: () => ({
search: null
}),
created () {
this.valueChanged = debounce(function (event) {
// Here you have access to `this`
this.makeAPIrequest(event.target.value)
}.bind(this), this.delay)
},
methods: {
makeAPIrequest (newVal) {
// ...
}
}
And the template:
<template>
//...
<input type="text" v-model="search" #input="valueChanged" />
//...
</template>
NOTE: in the example above I made an example of search input which can call the API with a custom delay which is provided in props
Although pretty much all answers here are already correct, if anyone is in search of a quick solution I have a directive for this.
https://www.npmjs.com/package/vue-lazy-input
It applies to #input and v-model, supports custom components and DOM elements, debounce and throttle.
Vue.use(VueLazyInput)
new Vue({
el: '#app',
data() {
return {
val: 42
}
},
methods:{
onLazyInput(e){
console.log(e.target.value)
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<script src="https://unpkg.com/lodash/lodash.min.js"></script><!-- dependency -->
<script src="https://unpkg.com/vue-lazy-input#latest"></script>
<div id="app">
<input type="range" v-model="val" #input="onLazyInput" v-lazy-input /> {{val}}
</div>
To create debounced methods you can use computeds, that way they won't be shared across multiple instances of your component:
<template>
<input #input="handleInputDebounced">
<template>
<script>
import debounce from 'lodash.debouce';
export default {
props: {
timeout: {
type: Number,
default: 200,
},
},
methods: {
handleInput(event) {
// input handling logic
},
},
computed: {
handleInputDebounced() {
return debounce(this.handleInput, this.timeout);
},
},
}
</script>
You can make it work with uncontrolled v-model as well:
<template>
<input v-model="debouncedModel">
<template>
<script>
import debounce from 'lodash.debouce';
export default {
props: {
value: String,
timeout: {
type: Number,
default: 200,
},
},
methods: {
updateValue(value) {
this.$emit('input', value);
},
},
computed: {
updateValueDebounced() {
return debounce(this.updateValue, this.timeout);
},
debouncedModel: {
get() { return this.value; },
set(value) { this.updateValueDebounced(value); }
},
},
}
</script>
Here is a vue3 way
...
<input v-model="searchInput">
...
setup(){
const searchInput = ref(null)
const timeoutID = ref(null)
watch(searchInput, (new, old) => {
clearTimeout(timeoutID.value)
timeoutID.value = setTimeout(() => {
//Call function for searching
}, 500) //millisecons before it is run
})
return {...}
}
If you are using Vue you can also use v.model.lazy instead of debounce but remember v.model.lazy will not always work as Vue limits it for custom components.
For custom components you should use :value along with #change.native
<b-input :value="data" #change.native="data = $event.target.value" ></b-input>
1 Short version using arrow function, with default delay value
file: debounce.js in ex: ( import debounce from '../../utils/debounce' )
export default function (callback, delay=300) {
let timeout = null
return (...args) => {
clearTimeout(timeout)
const context = this
timeout = setTimeout(() => callback.apply(context, args), delay)
}
}
2 Mixin option
file: debounceMixin.js
export default {
methods: {
debounce(func, delay=300) {
let debounceTimer;
return function() {
// console.log("debouncing call..");
const context = this;
const args = arguments;
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => func.apply(context, args), delay);
// console.log("..done");
};
}
}
};
Use in vueComponent:
<script>
import debounceMixin from "../mixins/debounceMixin";
export default {
mixins: [debounceMixin],
data() {
return {
isUserIdValid: false,
};
},
mounted() {
this.isUserIdValid = this.debounce(this.checkUserIdValid, 1000);
},
methods: {
isUserIdValid(id){
// logic
}
}
</script>
another option, example
Vue search input debounce
Here's an example Vue 2 component that demonstrates how to use debounce.
<template>
<div>
<v-btn #click="properDebounceMyMethod">Proper debounce</v-btn>
<v-btn #click="notWorkingDebounceMyMethod">!debounce</v-btn>
<v-btn #click="myMethod">normal call</v-btn>
</div>
</template>
<script lang="ts" >
import { defineComponent } from '#vue/composition-api';
import { debounce } from 'lodash';
export default defineComponent({
name: 'DebounceExample',
created() {
// debounce instance method dynamically on created hook
this.properDebounceMyMethod = debounce(this.properDebounceMyMethod, 500);
},
methods: {
properDebounceMyMethod(){
this.myMethod();
},
notWorkingDebounceMyMethod() {
debounce(this.myMethod, 500);
},
myMethod() {
console.log('hi from my method');
},
}
});
</script>
If you could move the execution of the debounce function into some class method you could use a decorator from the utils-decorators lib (npm install --save utils-decorators):
import {debounce} from 'utils-decorators';
class SomeService {
#debounce(500)
getData(params) {
}
}
I was able to use debounce with very little implementation.
I am using Vue 2.6.14 with boostrap-vue:
Add this pkg to your package.json: https://www.npmjs.com/package/debounce
Add this to main.js:
import { debounce } from "debounce";
Vue.use(debounce);
In my component I have this input:
<b-form-input
debounce="600"
#update="search()"
trim
id="username"
v-model="form.userName"
type="text"
placeholder="Enter username"
required
>
</b-form-input>
All it does is call the search() method and the search method uses the form.userName for perform the search.
<template>
<input type="text" v-model="search" #input="debouncedSearch" />
</template>
<script>
import _ from 'lodash';
export default {
data() {
return {
search: '',
};
},
methods: {
search() {
// Perform the search here
console.log(this.search);
},
},
created() {
this.debouncedSearch = _.debounce(this.search, 1000);
},
};
</script>
public debChannel = debounce((key) => this.remoteMethodChannelName(key), 200)
vue-property-decorator

How to implement debounce in Vue2?

I have a simple input box in a Vue template and I would like to use debounce more or less like this:
<input type="text" v-model="filterKey" debounce="500">
However the debounce property has been deprecated in Vue 2. The recommendation only says: "use v-on:input + 3rd party debounce function".
How do you correctly implement it?
I've tried to implement it using lodash, v-on:input and v-model, but I am wondering if it is possible to do without the extra variable.
In template:
<input type="text" v-on:input="debounceInput" v-model="searchInput">
In script:
data: function () {
return {
searchInput: '',
filterKey: ''
}
},
methods: {
debounceInput: _.debounce(function () {
this.filterKey = this.searchInput;
}, 500)
}
The filterkey is then used later in computed props.
I am using debounce NPM package and implemented like this:
<input #input="debounceInput">
methods: {
debounceInput: debounce(function (e) {
this.$store.dispatch('updateInput', e.target.value)
}, config.debouncers.default)
}
Using lodash and the example in the question, the implementation looks like this:
<input v-on:input="debounceInput">
methods: {
debounceInput: _.debounce(function (e) {
this.filterKey = e.target.value;
}, 500)
}
Option 1: Re-usable, no deps
- Recommended if needed more than once in your project
/helpers.js
export function debounce (fn, delay) {
var timeoutID = null
return function () {
clearTimeout(timeoutID)
var args = arguments
var that = this
timeoutID = setTimeout(function () {
fn.apply(that, args)
}, delay)
}
}
Typescript?
export function debounce<T extends (...args: any[]) => void>(fn: T, delay: number): T {
let timeoutID: number | null = null;
return function (this: any, ...args: any[]) {
clearTimeout(timeoutID);
timeoutID = setTimeout(() => {
fn.apply(this, args);
}, delay);
} as T;
}
Or if using a d.ts:
declare function debounce(fn: (...args: any[]) => void, delay: number): (...args: any[]) => void;
/Component.vue
<script>
import {debounce} from './helpers'
export default {
data () {
return {
input: '',
debouncedInput: ''
}
},
watch: {
input: debounce(function (newVal) {
this.debouncedInput = newVal
}, 500)
}
}
</script>
Codepen
Option 2: In-component, also no deps
- Recommended if using once or in small project
/Component.vue
<template>
<input type="text" v-model="input" />
</template>
<script>
export default {
data: {
timeout: null,
debouncedInput: ''
},
computed: {
input: {
get() {
return this.debouncedInput
},
set(val) {
if (this.timeout) clearTimeout(this.timeout)
this.timeout = setTimeout(() => {
this.debouncedInput = val
}, 300)
}
}
}
}
</script>
Codepen
Assigning debounce in methods can be trouble. So instead of this:
// Bad
methods: {
foo: _.debounce(function(){}, 1000)
}
You may try:
// Good
created () {
this.foo = _.debounce(function(){}, 1000);
}
It becomes an issue if you have multiple instances of a component - similar to the way data should be a function that returns an object. Each instance needs its own debounce function if they are supposed to act independently.
Here's an example of the problem:
Vue.component('counter', {
template: '<div>{{ i }}</div>',
data: function(){
return { i: 0 };
},
methods: {
// DON'T DO THIS
increment: _.debounce(function(){
this.i += 1;
}, 1000)
}
});
new Vue({
el: '#app',
mounted () {
this.$refs.counter1.increment();
this.$refs.counter2.increment();
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.16/vue.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.5/lodash.min.js"></script>
<div id="app">
<div>Both should change from 0 to 1:</div>
<counter ref="counter1"></counter>
<counter ref="counter2"></counter>
</div>
Very simple without lodash
handleScroll: function() {
if (this.timeout)
clearTimeout(this.timeout);
this.timeout = setTimeout(() => {
// your action
}, 200); // delay
}
I had the same problem and here is a solution that works without plugins.
Since <input v-model="xxxx"> is exactly the same as
<input
v-bind:value="xxxx"
v-on:input="xxxx = $event.target.value"
>
(source)
I figured I could set a debounce function on the assigning of xxxx in xxxx = $event.target.value
like this
<input
v-bind:value="xxxx"
v-on:input="debounceSearch($event.target.value)"
>
methods:
debounceSearch(val){
if(search_timeout) clearTimeout(search_timeout);
var that=this;
search_timeout = setTimeout(function() {
that.xxxx = val;
}, 400);
},
If you need a very minimalistic approach to this, I made one (originally forked from vuejs-tips to also support IE) which is available here: https://www.npmjs.com/package/v-debounce
Usage:
<input v-model.lazy="term" v-debounce="delay" placeholder="Search for something" />
Then in your component:
<script>
export default {
name: 'example',
data () {
return {
delay: 1000,
term: '',
}
},
watch: {
term () {
// Do something with search term after it debounced
console.log(`Search term changed to ${this.term}`)
}
},
directives: {
debounce
}
}
</script>
Please note that I posted this answer before the accepted answer. It's not
correct. It's just a step forward from the solution in the
question. I have edited the accepted question to show both the author's implementation and the final implementation I had used.
Based on comments and the linked migration document, I've made a few changes to the code:
In template:
<input type="text" v-on:input="debounceInput" v-model="searchInput">
In script:
watch: {
searchInput: function () {
this.debounceInput();
}
},
And the method that sets the filter key stays the same:
methods: {
debounceInput: _.debounce(function () {
this.filterKey = this.searchInput;
}, 500)
}
This looks like there is one less call (just the v-model, and not the v-on:input).
In case you need to apply a dynamic delay with the lodash's debounce function:
props: {
delay: String
},
data: () => ({
search: null
}),
created () {
this.valueChanged = debounce(function (event) {
// Here you have access to `this`
this.makeAPIrequest(event.target.value)
}.bind(this), this.delay)
},
methods: {
makeAPIrequest (newVal) {
// ...
}
}
And the template:
<template>
//...
<input type="text" v-model="search" #input="valueChanged" />
//...
</template>
NOTE: in the example above I made an example of search input which can call the API with a custom delay which is provided in props
Although pretty much all answers here are already correct, if anyone is in search of a quick solution I have a directive for this.
https://www.npmjs.com/package/vue-lazy-input
It applies to #input and v-model, supports custom components and DOM elements, debounce and throttle.
Vue.use(VueLazyInput)
new Vue({
el: '#app',
data() {
return {
val: 42
}
},
methods:{
onLazyInput(e){
console.log(e.target.value)
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<script src="https://unpkg.com/lodash/lodash.min.js"></script><!-- dependency -->
<script src="https://unpkg.com/vue-lazy-input#latest"></script>
<div id="app">
<input type="range" v-model="val" #input="onLazyInput" v-lazy-input /> {{val}}
</div>
To create debounced methods you can use computeds, that way they won't be shared across multiple instances of your component:
<template>
<input #input="handleInputDebounced">
<template>
<script>
import debounce from 'lodash.debouce';
export default {
props: {
timeout: {
type: Number,
default: 200,
},
},
methods: {
handleInput(event) {
// input handling logic
},
},
computed: {
handleInputDebounced() {
return debounce(this.handleInput, this.timeout);
},
},
}
</script>
You can make it work with uncontrolled v-model as well:
<template>
<input v-model="debouncedModel">
<template>
<script>
import debounce from 'lodash.debouce';
export default {
props: {
value: String,
timeout: {
type: Number,
default: 200,
},
},
methods: {
updateValue(value) {
this.$emit('input', value);
},
},
computed: {
updateValueDebounced() {
return debounce(this.updateValue, this.timeout);
},
debouncedModel: {
get() { return this.value; },
set(value) { this.updateValueDebounced(value); }
},
},
}
</script>
Here is a vue3 way
...
<input v-model="searchInput">
...
setup(){
const searchInput = ref(null)
const timeoutID = ref(null)
watch(searchInput, (new, old) => {
clearTimeout(timeoutID.value)
timeoutID.value = setTimeout(() => {
//Call function for searching
}, 500) //millisecons before it is run
})
return {...}
}
If you are using Vue you can also use v.model.lazy instead of debounce but remember v.model.lazy will not always work as Vue limits it for custom components.
For custom components you should use :value along with #change.native
<b-input :value="data" #change.native="data = $event.target.value" ></b-input>
1 Short version using arrow function, with default delay value
file: debounce.js in ex: ( import debounce from '../../utils/debounce' )
export default function (callback, delay=300) {
let timeout = null
return (...args) => {
clearTimeout(timeout)
const context = this
timeout = setTimeout(() => callback.apply(context, args), delay)
}
}
2 Mixin option
file: debounceMixin.js
export default {
methods: {
debounce(func, delay=300) {
let debounceTimer;
return function() {
// console.log("debouncing call..");
const context = this;
const args = arguments;
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => func.apply(context, args), delay);
// console.log("..done");
};
}
}
};
Use in vueComponent:
<script>
import debounceMixin from "../mixins/debounceMixin";
export default {
mixins: [debounceMixin],
data() {
return {
isUserIdValid: false,
};
},
mounted() {
this.isUserIdValid = this.debounce(this.checkUserIdValid, 1000);
},
methods: {
isUserIdValid(id){
// logic
}
}
</script>
another option, example
Vue search input debounce
Here's an example Vue 2 component that demonstrates how to use debounce.
<template>
<div>
<v-btn #click="properDebounceMyMethod">Proper debounce</v-btn>
<v-btn #click="notWorkingDebounceMyMethod">!debounce</v-btn>
<v-btn #click="myMethod">normal call</v-btn>
</div>
</template>
<script lang="ts" >
import { defineComponent } from '#vue/composition-api';
import { debounce } from 'lodash';
export default defineComponent({
name: 'DebounceExample',
created() {
// debounce instance method dynamically on created hook
this.properDebounceMyMethod = debounce(this.properDebounceMyMethod, 500);
},
methods: {
properDebounceMyMethod(){
this.myMethod();
},
notWorkingDebounceMyMethod() {
debounce(this.myMethod, 500);
},
myMethod() {
console.log('hi from my method');
},
}
});
</script>
If you could move the execution of the debounce function into some class method you could use a decorator from the utils-decorators lib (npm install --save utils-decorators):
import {debounce} from 'utils-decorators';
class SomeService {
#debounce(500)
getData(params) {
}
}
I was able to use debounce with very little implementation.
I am using Vue 2.6.14 with boostrap-vue:
Add this pkg to your package.json: https://www.npmjs.com/package/debounce
Add this to main.js:
import { debounce } from "debounce";
Vue.use(debounce);
In my component I have this input:
<b-form-input
debounce="600"
#update="search()"
trim
id="username"
v-model="form.userName"
type="text"
placeholder="Enter username"
required
>
</b-form-input>
All it does is call the search() method and the search method uses the form.userName for perform the search.
<template>
<input type="text" v-model="search" #input="debouncedSearch" />
</template>
<script>
import _ from 'lodash';
export default {
data() {
return {
search: '',
};
},
methods: {
search() {
// Perform the search here
console.log(this.search);
},
},
created() {
this.debouncedSearch = _.debounce(this.search, 1000);
},
};
</script>
public debChannel = debounce((key) => this.remoteMethodChannelName(key), 200)
vue-property-decorator