Vue.js watch a nested property inside array - javascript

I got a vue object like this:
var vm = new Vue({
el: '#app',
data: {
items: [],
index: 0
},
});
Inside items array i will push items like:
item1 = {
a: 1,
b: 'type',
c: '3.556'
}
...
itemN = {
a: n,
b: 'type',
c: '5.226'
}
then i will update one of the item's "c" property and i would like to set up a watcher that warn me as soon as one of this property changes.
EDIT: i also want to know witch item has changed

You can use deep watch, but... it does not provide ease way to determine which item has changed.
...
watch: {
items: {
handler: function (val, oldVal) {
},
deep: true
}
}
...
One of possible workarounds is mentioned in this answer,
idea behind this solution is to wrap each item in component and listen to event from component.
You can also store cloned items array and update that clone in watch handler, you can use that clone to filter item that has changed.

Related

How can I get Vue.js to be reactive for changes to dynamically added array properties?

In Vue.js, I have a data object with dynamically added/edited properties that are themselves arrays. For example, the property starts out as follows:
data: function () {
return {
vals: {}
};
}
And over time, through various button clicks, etc., vals may look like the following (with the actual property names and values being 100% dynamic based on a number of factors):
vals: {
set1: [
{
prop1: 123,
prop2: 'hello'
},
{
prop1: 456,
prop2: 'bye'
}
],
set2: [
{
prop3: 'Why?!',
prop4: false
}
]
}
As the array properties (i.e., set1 and set2) are changed, I want to be able to react to those changes.
For example, I may do something like the following in my code:
var prop = 'set1';
this.vals[prop].push({
{
prop1: 789,
prop2: 'hmmm...'
}
});
However, when I do that, the component is not updating (I presume because I am pushing an object onto the end of a subarray of an object; and Vue.js doesn't seem to track those changes).
I have been able to force the component to be reactive by doing this.$forceUpdate(); after the above push, but I imagine there has to be a more eloquent way of getting Vue.js to be reactive when it comes to objects being pushed onto the end of object subarrays.
Does anyone know of a better way to try to do what I am trying to achieve? Thank you.
Any time you're adding a new property to an object or changing a value within an array, you need to use Vue.set().
Vue.set(this.vals, prop, [ /* ... */ ])
Ideally, you should define all your properties up front so Vue doesn't have to invalidate computed properties depending on your data model's shape. Even if you have them set to null you should try to map out all the properties you expect your component to need.
Also, in your first code block, you have a colon after your return: which would evaluate to a label, meaning your data function isn't returning anything.
You could try something like this using lodash and a deep watcher..
More on deep watcher here
new Vue({
el: "#app",
methods: {
setValue() {
this.oldPeople = _.cloneDeep(this.people);
}
},
mounted() {
this.setValue();
},
el: "#app",
data: {
changed: null,
people: [
{ id: 0, name: "Bob", age: 27 },
{ id: 1, name: "Frank", age: 32 },
{ id: 2, name: "Joe", age: 38 }
],
oldPeople: []
},
watch: {
people: {
deep: true,
handler(after, before) {
// Return the object that changed
let vm = this;
let changed = after.filter(function(p, idx) {
return Object.keys(p).some(function(prop) {
return p[prop] !== vm.oldPeople[idx][prop];
});
});
vm.setValue();
this.changed = changed;
},
}
}
});
input {
display: block;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.1.6/vue.js"></script>
<script src="https://cdn.jsdelivr.net/lodash/4.17.2/lodash.min.js"></script>
<div id="app">
<div>
<input type="text" v-for="(person, index) in people" v-model="people[index].age" />
<div v-if="changed !== null">
You changed:<br/>{{ changed }}
</div>
</div>
</div>

Can Vue.js update a nested property?

I have a complicated component with lots of two way bound variables so in order to keep things clean im grouping variables in category objects.
settings: {
setting1: true,
setting2: false,
setting3: true
},
viewMode: {
option1: true,
option2: false,
options3: true
}
I am then passing the settings to my component like so
<some-component :settings.sync="settings" :viewmode.sync="viewMode"></some-component>
some-component then can transform these values and emit them back to the parent but this is where the problem lies. It appears
this.$emit('update:settings.setting1', newValue)
does NOT work in Vuejs. The only solution i can find to update these values from some-component is to overwrite the entire settings object like so
props: {
settings: {
type: Object,
default: () => {
return {
setting1: true,
setting2: false,
setting3: true
}
}
}
},
computed: {
localSetting1: {
get () {
return this.settings.setting1
},
set (newValue) {
// This does not work
this.$emit('update:settings.setting1', newValue)
// The only thing that does seem to work, is overwriting the entire object
this.$emit('update:settings', {
setting1: newValue,
setting2: this.settings.setting2,
setting3: this.settings.setting3
}
// or to be less verbose, but still update the entire object
this.$emit('update:settings', Object.assign(this.settings, {settings1: newValue}))
}
}
}
This seems a bit messy. Is there not a way to update just a single nested property and emit it back to the parent? The most ideal way being something similar to this
this.$emit('update:settings.setting1', newValue)
Actually Vue can update nested props, we just need to overwrite the whole prop object, as Vue cannot track nested changes.
If we have a prop like this
props: {
someValues: {
'a': 1,
'b': 2,
'c': 3
}
}
If we make a change like this one Vue will not react
this.someValues.a = 10
What we need to do
this.tmpSomeValues = { ...someValues, a: 10 }
this.someValues = this.tmpSomeValues

How to compare previous and updated props Vue?

I am getting an array from props, and i should trigger a function when the length of an array is changed. Due to the docs, i should use the following construction "handler: function (val, oldVal)", but it returns the new length.
props: ['array']
watch: {
array: function(val, oldVal) { // watch it
console.log('Prop changed: ', val, ' | was: ', oldVal) // it always returns the new array
}
}
Maybe i should use some of the lifecycle hooks?
Most of JavaScript's built-in array functionality mutates an array rather than creating a new array. Vue will detect the changes but the underlying behaviour remains the same, the array is modified.
For example, array.push(17) adds a new value to the end of the same array, it does not create a new array.
If you modify an array it will notify the watch function but the 'new' array is just the same object as the original array. So Vue is passing you the old array, you've just modified that array. Vue does not store a copy of the original state of that array.
From the 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’t keep a copy of the pre-mutate value.
If you just care about the length you could do something like this:
computed: {
arrayLength () {
return this.array.length
}
},
watch: {
arrayLength (newLength, oldLength) {
// ...
}
}
or more directly:
watch: {
'array.length' (newLength, oldLength) {
// ...
}
}
If you really need the old array then you'll either have to pass in a copy as the prop value, or take a copy within your component.
I don't see anything wrong with your code sample. Below is a working example.
Are you sure the array you are passing as a prop is changing?
Are you sure you are correctly mutating the array?
Check out Vuejs Docs - Array Caveats
Vue.component('child', {
template: '<div>Array length is {{ array.length }}</div>',
props: {
array: {
type: Array,
default: () => [],
},
},
watch: {
array: function(val, oldVal) {
alert(`changed from ${oldVal.length} to ${val.length}`);
},
},
});
new Vue({
el: '#app',
data() {
return {
arr: [1, 2, 3],
};
},
mounted() {
setTimeout(this.updateArr, 3000);
},
methods: {
updateArr() {
this.arr = [1, 2, 3, 4];
},
},
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.min.js"></script>
<div id="app">
<child :array="arr"></child>
</div>

Vue.js - How to properly watch for nested data

I'm trying to understand how to properly watch for some prop variation.
I have a parent component (.vue files) that receive data from an ajax call, put the data inside an object and use it to render some child component through a v-for directive, below a simplification of my implementation:
<template>
<div>
<player v-for="(item, key, index) in players"
:item="item"
:index="index"
:key="key"">
</player>
</div>
</template>
... then inside <script> tag:
data(){
return {
players: {}
},
created(){
let self = this;
this.$http.get('../serv/config/player.php').then((response) => {
let pls = response.body;
for (let p in pls) {
self.$set(self.players, p, pls[p]);
}
});
}
item objects are like this:
item:{
prop: value,
someOtherProp: {
nestedProp: nestedValue,
myArray: [{type: "a", num: 1},{type: "b" num: 6} ...]
},
}
Now, inside my child "player" component I'm trying to watch for any Item's property variation and I use:
...
watch:{
'item.someOtherProp'(newVal){
//to work with changes in "myArray"
},
'item.prop'(newVal){
//to work with changes in prop
}
}
It works but it seems a bit tricky to me and I was wondering if this is the right way to do it. My goal is to perform some action every time prop changes or myArray gets new elements or some variation inside existing ones. Any suggestion will be appreciated.
You can use a deep watcher for that:
watch: {
item: {
handler(val){
// do stuff
},
deep: true
}
}
This will now detect any changes to the objects in the item array and additions to the array itself (when used with Vue.set). Here's a JSFiddle: http://jsfiddle.net/je2rw3rs/
EDIT
If you don't want to watch for every change on the top level object, and just want a less awkward syntax for watching nested objects directly, you can simply watch a computed instead:
var vm = new Vue({
el: '#app',
computed: {
foo() {
return this.item.foo;
}
},
watch: {
foo() {
console.log('Foo Changed!');
}
},
data: {
item: {
foo: 'foo'
}
}
})
Here's the JSFiddle: http://jsfiddle.net/oa07r5fw/
Another good approach and one that is a bit more elegant is as follows:
watch:{
'item.someOtherProp': function (newVal, oldVal){
//to work with changes in someOtherProp
},
'item.prop': function(newVal, oldVal){
//to work with changes in prop
}
}
(I learned this approach from #peerbolte in the comment here)
VueJs deep watch in child objects
new Vue({
el: "#myElement",
data: {
entity: {
properties: []
}
},
watch: {
'entity.properties': {
handler: function (after, before) {
// Changes detected. Do work...
},
deep: true
}
}
});
Personally I prefer this clean implementation:
watch: {
myVariable: {
handler(newVal, oldVal){ // here having access to the new and old value
// do stuff
console.log(newVal, oldVal);
},
deep: true,
/*
Also very important the immediate in case you need it,
the callback will be called immediately after the start
of the observation
*/
immediate: true
}
}
How if you want to watch a property for a while and then to un-watch it?
Or to watch a library child component property?
You can use the "dynamic watcher":
this.$watch(
'object.property', //what you want to watch
(newVal, oldVal) => {
//execute your code here
}
)
The $watch returns an unwatch function which will stop watching if it is called.
var unwatch = vm.$watch('a', cb)
// later, teardown the watcher
unwatch()
Also you can use the deep option:
this.$watch(
'someObject', () => {
//execute your code here
},
{ deep: true }
)
Please make sure to take a look to docs
Another way to add that I used to 'hack' this solution was to do this:
I set up a seperate computed value that would simply return the nested object value.
data : function(){
return {
countries : {
UnitedStates : {
value: "hello world";
}.
},
};
},
computed : {
helperName : function(){
return this.countries.UnitedStates.value;
},
},
watch : {
helperName : function(newVal, oldVal){
// do this...
}
}
Tracking individual changed items in a list
If you want to watch all items in a list and know which item in the list changed, you can set up custom watchers on every item separately, like so:
var vm = new Vue({
data: {
list: [
{name: 'obj1 to watch'},
{name: 'obj2 to watch'},
],
},
methods: {
handleChange (newVal, oldVal) {
// Handle changes here!
// NOTE: For mutated objects, newVal and oldVal will be identical.
console.log(newVal);
},
},
created () {
this.list.forEach((val) => {
this.$watch(() => val, this.handleChange, {deep: true});
});
},
});
If your list isn't populated straight away (like in the original question), you can move the logic out of created to wherever needed, e.g. inside the .then() block.
Watching a changing list
If your list itself updates to have new or removed items, I've developed a useful pattern that "shallow" watches the list itself, and dynamically watches/unwatches items as the list changes:
// NOTE: This example uses Lodash (_.differenceBy and _.pull) to compare lists
// and remove list items. The same result could be achieved with lots of
// list.indexOf(...) if you need to avoid external libraries.
var vm = new Vue({
data: {
list: [
{name: 'obj1 to watch'},
{name: 'obj2 to watch'},
],
watchTracker: [],
},
methods: {
handleChange (newVal, oldVal) {
// Handle changes here!
console.log(newVal);
},
updateWatchers () {
// Helper function for comparing list items to the "watchTracker".
const getItem = (val) => val.item || val;
// Items that aren't already watched: watch and add to watched list.
_.differenceBy(this.list, this.watchTracker, getItem).forEach((item) => {
const unwatch = this.$watch(() => item, this.handleChange, {deep: true});
this.watchTracker.push({ item: item, unwatch: unwatch });
// Uncomment below if adding a new item to the list should count as a "change".
// this.handleChange(item);
});
// Items that no longer exist: unwatch and remove from the watched list.
_.differenceBy(this.watchTracker, this.list, getItem).forEach((watchObj) => {
watchObj.unwatch();
_.pull(this.watchTracker, watchObj);
// Optionally add any further cleanup in here for when items are removed.
});
},
},
watch: {
list () {
return this.updateWatchers();
},
},
created () {
return this.updateWatchers();
},
});
I've found it works this way too:
watch: {
"details.position"(newValue, oldValue) {
console.log("changes here")
}
},
data() {
return {
details: {
position: ""
}
}
}
Not seeing it mentioned here, but also possible to use the vue-property-decorator pattern if you are extending your Vue class.
import { Watch, Vue } from 'vue-property-decorator';
export default class SomeClass extends Vue {
...
#Watch('item.someOtherProp')
someOtherPropChange(newVal, oldVal) {
// do something
}
...
}
My problem with the accepted answer of using deep: true, is that when deep-watching an array, I can't easily identify which element of the array contains the change. The only clear solution I've found is this answer, which explains how to make a component so you can watch each array element individually.
None of the answer for me was working. Actually if you want to watch on nested data with Components being called multiple times. So they are called with different props to identify them.
For example <MyComponent chart="chart1"/> <MyComponent chart="chart2"/>
My workaround is to create an addionnal vuex state variable, that I manually update to point to the property that was last updated.
Here is a Vuex.ts implementation example:
export default new Vuex.Store({
state: {
hovEpacTduList: {}, // a json of arrays to be shared by different components,
// for example hovEpacTduList["chart1"]=[2,6,9]
hovEpacTduListChangeForChart: "chart1" // to watch for latest update,
// here to access "chart1" update
},
mutations: {
setHovEpacTduList: (state, payload) => {
state.hovEpacTduListChangeForChart = payload.chart // we will watch hovEpacTduListChangeForChart
state.hovEpacTduList[payload.chart] = payload.list // instead of hovEpacTduList, which vuex cannot watch
},
}
On any Component function to update the store:
const payload = {chart:"chart1", list: [4,6,3]}
this.$store.commit('setHovEpacTduList', payload);
Now on any Component to get the update:
computed: {
hovEpacTduListChangeForChart() {
return this.$store.state.hovEpacTduListChangeForChart;
}
},
watch: {
hovEpacTduListChangeForChart(chart) {
if (chart === this.chart) // the component was created with chart as a prop <MyComponent chart="chart1"/>
console.log("Update! for", chart, this.$store.state.hovEpacTduList[chart]);
},
},
I used deep:true, but found the old and new value in the watched function was the same always. As an alternative to previous solutions I tried this, which will check any change in the whole object by transforming it to a string:
created() {
this.$watch(
() => JSON.stringify(this.object),
(newValue, oldValue) => {
//do your stuff
}
);
},
For anyone looking for Vue 3
import { watch } from 'vue';
...
...
watch(
() => yourNestedObject, // first param, your object
(currValue, prevValue) => { // second param, watcher callback
console.log(currValue, prevValue);
},
{ deep: true } // third param, for deep checking
);
You can refer to the documentation here: https://v3.vuejs.org/guide/reactivity-computed-watchers.html#watch
Here's a way to write watchers for nested properties:
new Vue({
...allYourOtherStuff,
watch: {
['foo.bar'](newValue, oldValue) {
// Do stuff here
}
}
});
You can even use this syntax for asynchronous watchers:
new Vue({
...allYourOtherStuff,
watch: {
async ['foo.bar'](newValue, oldValue) {
// Do stuff here
}
}
});
https://vuejs.org/guide/essentials/watchers.html#deep-watchers
export default {
watch: {
someObject: {
handler(newValue, oldValue) {
// Note: `newValue` will be equal to `oldValue` here
// on nested mutations as long as the object itself
// hasn't been replaced.
},
deep: true
}
}
}

Re-render DOM if index of data elements changed

Vue.js doesnt detect that i swap 2 array elements in my data object:
data: {
list: [
'Foo',
'Bar',
'Test'
]
}
Method to swap entries:
swapIndex: function(from, to) {
var first = this.list[from];
this.list[from] = this.list[to];
this.list[to] = first;
}
JsFiddle https://jsfiddle.net/aaroniker/r11hxce8/
I want re-render the v-for loop if i swap the indexies.
Thanks!
here is the solution I came with. I created a copy of your list to modify it, and I invoked the this.$set() method on the list:
new Vue({
el: '#app',
data: {
list: [
'Foo',
'Bar',
'Test'
]
},
methods: {
swapIndex: function(from, to) {
var copy = JSON.parse(JSON.stringify(this.list))
var first = copy[from];
copy[from] = copy[to];
copy[to] = first;
this.$set(this,'list',copy)
console.log(this.list);
}
}
})
Changing value in an array with the [] operator won't let vue detect the change, replacing the array is one way to solve this, or you can use the slightly better solution of arr.splice(index, 1, newVal) suggested by the guide.

Categories