Vuejs 2: debounce not working on a watch option - javascript

When I debounce this function in VueJs it works fine if I provide the number of milliseconds as a primitive. However, if I provide it as a reference to a prop, it ignores it.
Here's the abbreviated version of the props:
props : {
debounce : {
type : Number,
default : 500
}
}
Here is the watch option that does NOT work:
watch : {
term : _.debounce(function () {
console.log('Debounced term: ' + this.term);
}, this.debounce)
}
Here is a watch option that DOES work:
watch : {
term : _.debounce(function () {
console.log('Debounced term: ' + this.term);
}, 500)
}
It suspect that it is a scope issue but I don't know how to fix it. If I replace the watch method as follows...:
watch : {
term : function () {
console.log(this.debounce);
}
}
... I get the correct debounce value (500) appearing in the console.

Another variation to #Bert's answer is to build the watcher's function in created(),
// SO: Vuejs 2: debounce not working on a watch option
console.clear()
Vue.component("debounce",{
props : {
debounce : {
type : Number,
default : 500
}
},
template:`
<div>
<input type="text" v-model="term">
</div>
`,
data(){
return {
term: "",
debounceFn: null
}
},
created() {
this.debounceFn = _.debounce( () => {
console.log('Debounced term: ' + this.term);
}, this.debounce)
},
watch : {
term : function () {
this.debounceFn();
}
},
})
new Vue({
el: "#app"
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.3/vue.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.4/lodash.min.js"></script>
<div id="app">
<debounce :debounce="2000"></debounce>
</div>
Example on CodePen

The primary issue here is using this.debounce as the interval when defining your debounced function. At the time _.debounce(...) is run (when the component is being compiled) the function is not yet attached to the Vue, so this is not the Vue and this.debounce will be undefined. That being the case, you will need to define the watch after the component instance has been created. Vue gives you the ability to do that using $watch.
I would recommend you add it in the created lifecycle handler.
created(){
this.unwatch = this.$watch('term', _.debounce((newVal) => {
console.log('Debounced term: ' + this.term);
}, this.debounce))
},
beforeDestroy(){
this.unwatch()
}
Note above that the code also calls unwatch which before the component is destroyed. This is typically handled for you by Vue, but because the code is adding the watch manually, the code also needs to manage removing the watch. Of course, you will need to add unwatch as a data property.
Here is a working example.
console.clear()
Vue.component("debounce",{
props : {
debounce : {
type : Number,
default : 500
}
},
template:`
<input type="text" v-model="term">
`,
data(){
return {
unwatch: null,
term: ""
}
},
created(){
this.unwatch = this.$watch('term', _.debounce((newVal) => {
console.log('Debounced term: ' + this.term);
}, this.debounce))
},
beforeDestroy(){
this.unwatch()
}
})
new Vue({
el: "#app"
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.4/lodash.min.js"></script>
<script src="https://unpkg.com/vue#2.4.2"></script>
<div id="app">
<debounce :debounce="250"></debounce>
</div>

The debounced method needs to be abstracted since we need to call the same function everytime the watch is triggered. If we place the debounced method inside a Vue computed or watch property, it will be renewed everytime.
const debouncedGetData = _.debounce(getData, 1000);
function getData(val){
this.newFoo = val;
}
new Vue({
el: "#app",
template: `
<div>
<input v-model="foo" placeholder="Type something..." />
<pre>{{ newFoo }}</pre>
</div>
`,
data(){
return {
foo: '',
newFoo: ''
}
},
watch:{
foo(val, prevVal){
debouncedGetData.call(this, val);
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.9.1/underscore-min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div id="app"></div>
Good Luck...

new Vue({
el: '#term',
data: function() {
return {
term: 'Term',
debounce: 1000
}
},
watch: {
term : _.debounce(function () {
console.log('Debounced term: ' + this.term);
}, this.debounce)
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.4/lodash.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.3/vue.js"></script>
<div id="term">
<input v-model="term">
</div>

Related

v-on click, add handler only if condition has been met

After some research the following suggestion by Mr. Evan You was found:
https://github.com/vuejs/vue/issues/7349#issuecomment-354937350
So without any hesitation I gave it a try:
Component template
<template>
<div v-on='{ click: dataType === `section` ? toggleSectionElements : null }'>
... magic
</div>
<template>
JS Logic
<script>
export default {
name: `product-section`,
props: [`section`, `sectionName`, `depth`],
methods: {
toggleSectionElements() {
... magic
}
},
computed: {
dataType() {
if (this.$props.section.sections || this.$props.depth === 0) {
return `section`
} else {
return `element`
}
}
}
}
</script>
But for described case it results in error during rendering:
[Vue warn]: Invalid handler for event "click": got null
Can someone please suggest what has been done wrong? :thinking:
Update
The way Data Model looks like:
DataModel: {
mainSectionA: {
sections: {
sectionA: {
sections: {
elementA: { values: { ... } },
elementB: { values: { ... } }
}
values: { ... }
}
sectionB: {
elementA: { values: { ... } },
elementB: { values: { ... } }
}
},
values: { ... }
},
mainSectionB: {
sections: {
elementA: { values: { ... } },
elementB: { values: { ... } },
elementC: { values: { ... } },
... elements
},
values: { ... }
}
}
Just change it to the below and it will work
v-on="condition ? { mouseover: handler } : {}"
or, if your handler is called mouseover
v-on="condition ? { mouseover } : {}"
Instead of polluting your template with ternary logic, you should actually perform the check inside the click handler instead. It not only makes your template more readable, but also makes maintaining the code easier since all logic has been abstracted and delegated to the event handler's callback instead.
Quick solution
Therefore the quick solution is to actually ensure that the toggleSectionElements() will only work when a correct dataType is present. This can be achieved by using a guard clause:
toggleSectionElements() {
// Guard clause to prevent further code execution
if (this.dataType() !== 'section')
return;
// Magic here
}
Even better, is that if separate handlers should be assigned to each dataType: you can then create a factory function for that purpose:
methods: {
// This is just a factory function
toggleElements() {
switch (this.dataType()) {
case 'section':
return this.toggleSectionElements;
case 'element':
// Something else...
}
},
toggleSectionElements() {
// Magic for section element
}
}
Suggestion: using atomic components
Since it might be costly to bind click event handlers to elements that end up doing nothing, you can also break down your component to be more atomic. The collection element will be responsible of receiving an array of "section" or "element", and each "section"/"element" will have its own component, something like this:
You have a collection component, say <my-collection>, that holds all "section" and "element" components
"section" component will use the <my-section> component
"element" component will use the <my-element> component
This is when VueJS becomes really powerful: you can use dynamic component inside <my-collection> to determine which component to use depending on the dataType encountered.
This is done by running a v-for through the collection, and then using v-bind:is="..." to determine whether a specific collection item should be using "section" or "element". I understand that this is probably going to go out of scope of your original question, but it's a worthwhile design to consider:
const collectionComponent = Vue.component('my-collection', {
template: '#my-collection-component',
data: function() {
return {
collection: [{
dataType: 'section',
description: 'Hello I am section 1'
}, {
dataType: 'element',
description: 'Hello I am element 1'
}, {
dataType: 'section',
description: 'Hello I am section 2'
}, {
dataType: 'element',
description: 'Hello I am element 2'
}]
}
},
methods: {
componentToUse(dataType) {
return 'my-' + dataType;
}
}
});
const sectionComponent = Vue.component('my-section', {
template: '#my-section-component',
props: ['itemData'],
methods: {
toggle() {
console.log('Doing some magic.');
}
}
});
const elementComponent = Vue.component('my-element', {
template: '#my-element-component',
props: ['itemData']
});
new Vue({
el: '#app'
});
.box {
border: 1px solid #999;
cursor: pointer;
margin: 10px;
padding: 10px;
}
.box:hover {
background-color: #eee;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<my-collection />
</div>
<script type="text/x-template" id="my-collection-component">
<div>
<component
v-for="(item, i) in collection"
v-bind:key="i"
v-bind:is="componentToUse(item.dataType)"
v-bind:itemData="item" />
</div>
</script>
<script type="text/x-template" id="my-section-component">
<div #click="toggle" class="box">
<h1>{{ itemData.dataType }}</h1>
<p>{{ itemData.description }}</p>
<p>Clicking on me will invoke a section-specific logic</p>
</div>
</script>
<script type="text/x-template" id="my-element-component">
<div class="box">
<h1>{{ itemData.dataType }}</h1>
<p>{{ itemData.description }}</p>
<p>Clicking on me will do nothing</p>
</div>
</script>
here:
click: dataType === `section` ? toggleSectionElements : null
in the not-equal case you pass null, but the value on click expects a function. you can try an emptry function:
click: dataType === `section` ? toggleSectionElements : ()=>{}
In Vue 3 you can pass null to the listener. Combining it with optional chaining you can do this:
#click="handler?.() || null"
Same for old browsers:
#click="handler ? handler() : null"

How can I convert a long list of 'watch' into a functional way in VueJS?

I'm a novice for vueJS.
I have a long list of watch list. It's all same.
But I don't know how to convert them into a functional way.
They are all for adding comma in input tag and v-model.
It works very well. But the codes look very dumb because they are exactly the same, but not their name.
new Vue({
data: {
tmp_price1: '',
tmp_price2: '',
tmp_price3: '',
tmp_a_price: '',
tmp_b_price: '',
},
watch: {
tmp_price1: function(newValue) {
if (newValue != '') {
const result = newValue.replace(/\D/g, "").replace(/\B(?=(\d{3})+(?!\d))/g, ",");
Vue.nextTick(() => this.tmp_price1 = result);
}
},
tmp_price2: function(newValue) {
if (newValue != '') {
const result = newValue.replace(/\D/g, "").replace(/\B(?=(\d{3})+(?!\d))/g, ",");
Vue.nextTick(() => this.tmp_price2 = result);
}
},
....(repeat)
},
Please help me improve these dumb codes efficient way.
It may seem like over-engineering, but I would probably make a component to encapsulate the commafying behavior. The component would have a settable computed in it that emits the commafied value for the parent to use in updating.
new Vue({
el: '#app',
data: {
tmp_price1: '',
tmp_price2: '',
tmp_price3: '',
tmp_a_price: '',
tmp_b_price: '',
},
components: {
commafiedInput: {
props: ['value'],
template: '<input v-model="commaValue">',
computed: {
commaValue: {
get() {
return this.value;
},
set(newValue) {
this.$emit('input', this.addCommas(newValue));
}
}
},
methods: {
addCommas(v) {
return v.replace(/\D/g, '').replace(/\B(?=(\d{3})+(?!\d))/g, ",");
}
}
}
}
});
<script src="https://unpkg.com/vue#latest/dist/vue.js"></script>
<div id="app">
<div> {{tmp_price1}}
<commafied-input v-model="tmp_price1"></commafied-input>
</div>
<commafied-input v-model="tmp_price2"></commafied-input>
<commafied-input v-model="tmp_price3"></commafied-input>
<commafied-input v-model="tmp_a_price"></commafied-input>
<commafied-input v-model="tmp_b_price"></commafied-input>
</div>
Just displaying the formatted version of the value can be done with a simple filter:
new Vue({
el: '#app',
data: {
tmp_price1: '123123',
tmp_price2: '',
tmp_price3: ''
},
filters: {
myFilter(v) {
return v.replace(/\D/g, '').replace(/\B(?=(\d{3})+(?!\d))/g, ",");
}
}
});
<script src="https://unpkg.com/vue#latest/dist/vue.js"></script>
<div id="app">
<div><input v-model="tmp_price1">{{tmp_price1 | myFilter}}</div>
<div><input v-model="tmp_price2">{{tmp_price2 | myFilter}}</div>
<div><input v-model="tmp_price3">{{tmp_price3 | myFilter}}</div>
</div>
...but that's not enough for the input fields; you can't just throw a filter onto the v-model attribute. A sub-component as described in Roy J's answer is probably the best and most reusable way to handle that, but if you're ok with something a bit quick-and-dirty (but not too dirty) you could just throw a change handler at the problem:
new Vue({
el: '#app',
data: {
tmp_price1: '123123',
tmp_price2: '',
tmp_price3: ''
},
methods: {
myFormatter(fieldname) {
/* replace the user's input with the formatted value.
There's probably some clever way to read the v-model
name from the input field instead of passing it to the
method as a string, but I'm not going to mess around
with that for what is after all a quick-and-dirty technique */
this[fieldname] = this[fieldname].replace(/\D/g, "").replace(/\B(?=(\d{3})+(?!\d))/g, ",");
}
},
mounted() {
// if the initial values aren't always empty, you'll need to run the
// formatter function on component load as well as on user input:
['tmp_price1', 'tmp_price2', 'tmp_price3'].forEach(f => {
this.myFormatter(f);
});
}
});
<script src="https://unpkg.com/vue#latest/dist/vue.js"></script>
<div id="app">
<div><input v-model="tmp_price1" #input="myFormatter('tmp_price1')">{{tmp_price1}}</div>
<div><input v-model="tmp_price2" #input="myFormatter('tmp_price2')">{{tmp_price2}}</div>
<div><input v-model="tmp_price3" #input="myFormatter('tmp_price3')">{{tmp_price3}}</div>
</div>
(Or as another variation, this answer to a similar question uses essentially the same technique, but wraps it in a Vue directive to bind the input events instead of attaching #input handlers.)
Note that in the "filter" version, the v-model values remain the original user input; the filter only affects the displayed value. In the second example the formatting is applied to the v-modeled values themselves, so if you pass those values on to somewhere else you'll get the formatted version. Both techniques can be useful, depending on the circumstances. (Or you could even use them in combination -- remove non-digits in the v-model, then add the commas via a filter just for display, for example.)

Passing data to one of many components

I am trying to create a DownloadButton component in VueJS that animates when clicked, and stops animating upon completion of the download. The DownloadButton component will be used in a table where its repeated many times. I want the download method to be contained in the parent. The problem is that changing the loading variable causes all the components to be affected rather than just the one being clicked.
Parent:
<DownloadButton #click.native="download" :loading="loading"></DownloadButton>
<DownloadButton #click.native="download" :loading="loading"></DownloadButton>
<DownloadButton #click.native="download" :loading="loading"></DownloadButton>
methods: {
download() {
this.loading = true
// wait for the download procedure to finish...
this.loading = false
}
}
You should monitor loading state of each button not just global loading.
Here is quick and simple example of what you want I think:
Vue.component("download-button", {
template: "#dbTemplate",
props: ['loading'],
computed: {
stateText() {
return this.loading ? 'Loading...' : 'Load';
}
}
});
new Vue({
el: "#app",
data: {
resources: [
{ date: new Date(), url: "some-url1" },
{ date: new Date(), url: "some-url2" },
{ date: new Date(), url: "some-url3" },
{ date: new Date(), url: "some-url4" }
],
resourceStates: {}
},
methods: {
downloadResource(resource) {
this.$set(this.resourceStates, resource.url, true);
new Promise((resolve, reject) => {
setTimeout(() => resolve(new Date()), 1000);
}).then((date) => {
resource.date = date;
this.$set(this.resourceStates, resource.url, false);
})
},
isLoading(resource) {
return !!this.resourceStates[resource.url];
}
}
});
<script src="https://unpkg.com/vue#2.5.16/dist/vue.js"></script>
<div id="app">
<div v-for="res in resources" :key="res.url" style="padding: 10px 0">
{{ res.date.toLocaleString() }}
<download-button #click.native="downloadResource(res)" :loading="isLoading(res)">
</download-button>
</div>
</div>
<script type="text/template-x" id="dbTemplate">
<button :disabled="loading">
{{ stateText }}
</button>
</script>

Is there any way to have multiple Vues have a computed listener working on the same value?

Setup:
I have multiple Vue components, and each component has multiple instances in different dialogs in my web app.
For each type of component I have a global state (handrailOptions in the example below) so that each type of component stays in sync across the dialogs.
I'd like for it so that when a component proceeds beyond step 1, I hide the other components in that dialog.
I have achieved this nicely using the computed / watch combo.
However, my problem is that it seems if I try to listen in with computed through more than 1 Vue instance, it hijacks the other listeners.
Problem
Below is a simplified version of what I'm working with, when the app starts up, the console logs 'computed 1' & 'computed 2'. But then when I change handrailOptions.step, only the second fires. ('computed 2' & 'watched 2')
Is there any way to have multiple Vues have a computed listener working on the same value?
handrailOptions = {
step: 1
};
Vue.component( 'handrail-options', {
template: '#module-handrail-options',
data: function() {
return handrailOptions;
},
});
var checkoutDialog = new Vue({
el: '#dialog-checkout',
computed: {
newHandrailStep() {
console.log('computed 1');
return handrailOptions.step;
}
},
watch: {
newHandrailStep( test ) {
console.log('watched 1');
}
}
});
new Vue({
el: '#dialog-estimate-questions',
computed: {
newHandrailStep() {
console.log('computed 2');
return handrailOptions.step;
}
},
watch: {
newHandrailStep( test ) {
console.log('watched 2');
}
}
});
This works as expected. I made handrailOptions responsive by making the data object of a new Vue. Making it the data object of a component, as you did, could also work, but the component would have to be instantiated at least once. It makes more sense to have a single object for your global, anyway.
handrailOptions = {
step: 1
};
// Make it responsive
new Vue({data: handrailOptions});
var checkoutDialog = new Vue({
el: '#dialog-checkout',
computed: {
newHandrailStep() {
console.log('computed 1', handrailOptions.step);
return handrailOptions.step;
}
},
watch: {
newHandrailStep(test) {
console.log('watched 1');
}
}
});
new Vue({
el: '#dialog-estimate-questions',
computed: {
newHandrailStep() {
console.log('computed 2', handrailOptions.step);
return handrailOptions.step;
}
},
watch: {
newHandrailStep(test) {
console.log('watched 2');
}
}
});
setInterval(() => ++handrailOptions.step, 1500);
<script src="//cdnjs.cloudflare.com/ajax/libs/vue/2.3.4/vue.min.js"></script>
<div id="dialog-estimate-questions">
Main step {{newHandrailStep}}
</div>
<div id="dialog-checkout">
CD step {{newHandrailStep}}
</div>

Vue.js with laravel 5.3

I'm using Laravel 5.3 with Vue.js(very new to this).
Here's my current code
app.js
var vm = new Vue({
el: '#app',
data: {
messages: []
},
ready: function(){
this.getMessages();
},
methods: {
getMessages: function(){
this.$http.get('api/messages').then((response) => {
this.$set('messages', data);
}, (response) => {
});
}
}
});
api.php route is very simple
Route::get('/messages', function() {
return Message::latest()->get();
});
Note: here when i try access the route directly as localhost:8000/api/messages i get the array with the full data
On my view i have
<div class="content" id="app">
<tr v-for="message in messages">
<td> #{{ message}} </td>
</tr>
</div>
I have included online libraries for all jquery, vue.js, and vue.resource.
When i use vue.js debugger it shows that it returns messages[] but it's empty.
I have followed a lot of examples but couldn't get it to work.
Any help is greatly appreciated
if you are using vue.js 2.0 , ready is deprecated now, you may use mounted instead
mounted: function () {
this.$nextTick(function () {
this.getMessages();
})
}
Vue.js Docs
Since you are using the arrow syntax, then I switched to full ES2015 Code
getMessages() {
this.$http.get('api/messages')
.then( result => {
this.messages = result.json()
})
}
Try this:
var vm = new Vue({
el: '#app',
data: {
messages: []
},
ready: function(){
this.getMessages();
},
methods: {
getMessages: function(){
let ctrl = this;
this.$http.get('api/messages').then((response) => {
this.messages = response.data;
});
}
}
});

Categories