I have a component with these options:
{
data() {
return {
...
controls: {
...
showProduct: {
11: true,
13: true,
15: false
}
}
}
},
watch: {
...
'controls.showProduct': {
handler(newValue, oldValue) { // some function },
deep: true
}
}
}
Whenever I change the value of any product (i.e. controls.showProduct[ x ]) the watcher fires, but newValue and oldValue are always the same with controls.showProduct[ x ] set to the new value.
See the note in api docs: http://vuejs.org/api/#vm-watch
There it is... plain as day. I'm sorry that I missed that. Excellent work on this platform. It's been an absolute pleasure to build with it.
So... is no ways more to detect what part of object changed inside watch? real target path like third param will be useful option... "controls.showProduct.15"
Agreed. It would be nice to be able to tell what changed in the array.
if you want to push to watching array for instance, you can copy this array, then push to this copy, and then assign this copy to your watching array back. this is very simple.
// copy array which you want to watch
let copiedArray = this.originArray.slice(0)
// push to this copy
copiedArray.push("new value");
// assign this copy to your watching array
this.originArray = copiedArray;
@uncleGena explain a little more please, it could help many of us
@BernardMarieOnzo according to docs:
Note: when mutating (rather than replacing) an Object or an Array, the old value will be the same as new value because they reference the same Object/Array. Vue doesn鈥檛 keep a copy of the pre-mutate value.
So what's @uncleGena doing to get the old value, is:
Creating a copy of the existing array to another variable, this will create a new value reference in memory
let copiedArray = this.originArray.slice(0)
Whatever you wanna add to the original array you had to the new variable you just created instead
copiedArray.push("new value");
Now you replace the old array with the new one, replacing the reference in memory from this.originArray to copiedArray, thus Vue will have both references of both variables and you will be able to get both new and old value.
this.originArray = copiedArray;
For a more clear understanding of why it works like this:
Basically when saving a variable object or array, Javascript doesn't keep the record of the value itself, instead it keeps a reference to its location in the Memory, so when you change an array or a object you just update the value in memory but your old and new value of this variable will be the same (the reference to its memory location)
thank you @dynalz and @uncleGena
I'm trying to watch changes that occur based on input fields so the workaround suggested doesn't apply. It would be really nice if this limitation was removed from the API, so the newValue and oldValue properties reflect mutations on bound objects.
As a example:
I have a list of nested tables (https://vuetifyjs.com/en/components/data-tables) that each have inline editable properties (https://vuetifyjs.com/en/components/data-tables#example-editdialog).
I should be able to watch my root array with deep set to true and diff for changes.
im facing exactly the same issue as @darkomenz .... :(
i need to add a new item in the array based on previous selection.
I'm also facing the issue with nested properties changed by input so the workaround isn't applicable. It's a large limitation of Vue to supply the old values. I will have to setup a cached value myself and refer back to that - what a puke-inducing hack. Other less-than-ideal alternative workarounds:
Anyone have any better ideas?
@ljelewis
If you have all the data in an object (like formData), you may use dynamic watch to watch all 27 inputs in your case.
You can add all inputs in your form to watchers after they are initialized.
e.g.
for (let k in this.formData) {
this.$watch('formData.' + k, function (val, oldVal) { console.log(k, val, oldVal) })
}
@aidangarza
@darkomenz
For this problem where it's only one or two properties that I need to watch, I used a computed property along with watch. Something like the following works:
var vm = new Vue({
el: "#app",
data: {
model: {
title: null,
description: null,
id: null,
createdDate: null
},
message: null
},
computed: {
// I just want to watch title for now
title: function() {
return this.model.title;
}
},
watch: {
title: function(n, o) {
if (n !== o) {
this.message = "Title updated from \"" + o + "\" to \"" + n + "\""
}
}
}
});
And also, if you are using v-model
on your input but are frustrated because it changes on each input event, change your declaration to v-model.lazy
. This fires after a change event rather than input.
@aidangarza
@darkomenz
If you need to watch the entire model, make the entire model a computed property and just use Object.assign({}, model)
like the following:
var vm = new Vue({
el: "#app",
data: {
model: {
title: null,
description: null,
id: null,
createdDate: null
},
message: null
},
computed: {
// watch the entire as a new object
computedModel: function() {
return Object.assign({}, this.model);
}
},
watch: {
computedModel: {
deep: true,
handler: function(n, o) {
var message = "";
if (n.title !== o.title) {
message += "<p>Title updated from \"" + o.title + "\" to \"" + n.title + "\"</p>"
}
if (n.description !== o.description) {
message += "<p>Description updated from \"" + o.description + "\" to \"" + n.description + "\"</p>"
}
this.message = message;
}
}
}
});
Computed properties are simply cached values returned from functions so since we are overwriting the cached object of the computed property with a brand new object rather than mutating a property within the original object, we are able to achieve the results we want. Noted: Object.assign polyfill required for some browsers.
some how if we use computed state, if we use it with vue and design, the value of input cannot change.
my only solutions right now is to maintain 2 copy of the value, both using object.assign({}, values)
and then compare the new value with the original copy
@someshinyobject You are a star, thank you!
@someshinyobject his solution is great, it didn't worked for me since I had a couple of json objects inside of it. I stringify the object in computed state and parse inside the watcher. This way it simply has to compare a string instead of a whole object.
I did a trick with computed properties, to transform the object into a JSON string, and I watch this string to detect changes. Here is a simple example :
{
data() {
return {
yourObjectOrArray: {}
}
},
computed: {
yourObjectOrArray_str: function () {
return JSON.stringify(this.yourObjectOrArray)
}
},
watch: {
yourObjectOrArray_str: function(newValue_str, oldValue_str) {
let newValue = JSON.parse(newValue_str)
let oldValue = JSON.parse(oldValue_str)
// some instructions
},
}
}
Here's a modification of @someshinyobject comment (requires lodash):
It watches all properties of the object not just the first level and also finds the changed property for you:
data: {
model: {
title: null,
props: {
prop1: 1,
prop2: 2,
},
}
},
watch: {
computedModel: {
deep: true,
handler(newValue, oldValue) => {
console.log('Change: ', this.getDifference(oldValue, newValue))
}
}
},
computed: {
computedModel() {
return lodash.cloneDeep(this.model)
}
},
methods: {
getDifference() {
function changes(object, base) {
return lodash.transform(object, function(result, value, key) {
if (!lodash.isEqual(value, base[key])) {
result[key] = (lodash.isObject(value) && lodash.isObject(base[key])) ? changes(value, base[key]) : value
}
})
}
return changes(object, base)
}
},
@aidangarza
@darkomenzIf you need to watch the entire model, make the entire model a computed property and just use
Object.assign({}, model)
like the following:var vm = new Vue({ el: "#app", data: { model: { title: null, description: null, id: null, createdDate: null }, message: null }, computed: { // watch the entire as a new object computedModel: function() { return Object.assign({}, this.model); } }, watch: { computedModel: { deep: true, handler: function(n, o) { var message = ""; if (n.title !== o.title) { message += "<p>Title updated from \"" + o.title + "\" to \"" + n.title + "\"</p>" } if (n.description !== o.description) { message += "<p>Description updated from \"" + o.description + "\" to \"" + n.description + "\"</p>" } this.message = message; } } } });
Computed properties are simply cached values returned from functions so since we are overwriting the cached object of the computed property with a brand new object _rather than_ mutating a property within the original object, we are able to achieve the results we want. Noted: Object.assign polyfill required for some browsers.
This is a good solution but if we would like to prevent change, we need to know which field was changed and pass the old param there... If there a way to know which filed was changed?
...
data() {
return {
shop: {
cost: 0,
name: ''
}
}
},
watch: {
'shop.cost': function (o,n) {
this.onChangeCost(o,n)
}
},
...
@aidangarza
@darkomenzIf you need to watch the entire model, make the entire model a computed property and just use
Object.assign({}, model)
like the following:var vm = new Vue({ el: "#app", data: { model: { title: null, description: null, id: null, createdDate: null }, message: null }, computed: { // watch the entire as a new object computedModel: function() { return Object.assign({}, this.model); } }, watch: { computedModel: { deep: true, handler: function(n, o) { var message = ""; if (n.title !== o.title) { message += "<p>Title updated from \"" + o.title + "\" to \"" + n.title + "\"</p>" } if (n.description !== o.description) { message += "<p>Description updated from \"" + o.description + "\" to \"" + n.description + "\"</p>" } this.message = message; } } } });
Computed properties are simply cached values returned from functions so since we are overwriting the cached object of the computed property with a brand new object _rather than_ mutating a property within the original object, we are able to achieve the results we want. Noted: Object.assign polyfill required for some browsers.
Just for reference, anybody looking to do this, Object.assign
will only prevent top-level change prevention, if you want to handle deeper levels, use JSON.stringify
or even JSON.parse( JSON.stringify( value ) )
or something from lodash, etc.
My assumption this would also reflect in Vue computed property, but wanted to mention this as this was a gotcha for me a while back with Object.assign
Most helpful comment
@aidangarza
@darkomenz
If you need to watch the entire model, make the entire model a computed property and just use
Object.assign({}, model)
like the following:Computed properties are simply cached values returned from functions so since we are overwriting the cached object of the computed property with a brand new object rather than mutating a property within the original object, we are able to achieve the results we want. Noted: Object.assign polyfill required for some browsers.
Extended example here: http://jsfiddle.net/1fw0357q/