Farzher
Farzher

Reputation: 14563

vue.js $watch array of objects

mounted: function() {
  this.$watch('things', function(){console.log('a thing changed')}, true);
}

things is an array of objects [{foo:1}, {foo:2}]

$watch detects when an object is added or removed, but not when values on an object are changed. How can I do that?

Upvotes: 51

Views: 122769

Answers (5)

Jurass
Jurass

Reputation: 596

If your intention is to render and array and watch for changes on rendered items, you can do this:

  1. Create new Component:

const template = `<div hidden></div>`
export default {
    template,
    props: ['onChangeOf'],
    emits: ['do'],
    watch: {
        onChangeOf: {
            handler(changedItem) {
                console.log('works')
                this.$emit('do', changedItem)
            },
            deep: true
        }
    },
}

  1. Register that component:

Vue.component('watcher', watcher)

  1. Use it inside of your foreach rendering:

<tr v-for="food in $store.foods" :key="food.id">
    <watcher :onChangeOf="food" @do="(change) => food.name = 'It works!!!'"></watcher>
</tr>

Upvotes: 0

Taher
Taher

Reputation: 12040

You can watch each element in an array or dictionary for change independently with $watch('arr.0', () => {}) or $watch('dict.keyName', () => {})

from https://v2.vuejs.org/v2/api/#vm-watch:

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.

However, you can iterate the dict/array and $watch each item independently. ie. $watch('foo.bar') - this watches changes in the property 'bar' of the object 'foo'.

In this example, we watch all items in arr_of_numbers, also 'foo' properties of all items in arr_of_objects:

mounted() {
        this.arr_of_numbers.forEach( (index, val) => {
            this.$watch(['arr_of_numbers', index].join('.'), (newVal, oldVal) => {
                console.info("arr_of_numbers", newVal, oldVal);
            });
        });

        for (let index in this.arr_of_objects) {
            this.$watch(['arr_of_objects', index, 'foo'].join('.'), (newVal, oldVal) => {
                console.info("arr_of_objects", this.arr_of_objects[index], newVal, oldVal);
            });
        }

    },
    data() {
        return {
            arr_of_numbers: [0, 1, 2, 3],
            arr_of_objects: [{foo: 'foo'}, {foo:'bar'}]
        }
    }

Upvotes: 8

Jonatan Machado
Jonatan Machado

Reputation: 128

If someone needs to get an item that was changed inside the array, please, check it:

JSFiddle Example

The post example code:

new Vue({
  ...
  watch: {
    things: {
      handler: function (val, oldVal) {
        var vm = this;
        val.filter( function( p, idx ) {
            return Object.keys(p).some( function( prop ) {
                var diff = p[prop] !== vm.clonethings[idx][prop];
                if(diff) {
                    p.changed = true;                        
                }
            })
        });
      },
      deep: true
    }
  },
  ...
})

Upvotes: 9

Aur&#233;lien  Lambert
Aur&#233;lien Lambert

Reputation: 730

There is a more simple way to watch an Array's items without having deep-watch: using computed values

{
  el: "#app",
  data () {
    return {
      list: [{a: 0}],
      calls: 0,
      changes: 0,
    }
  },
  computed: {
    copy () { return this.list.slice() },
  },
  watch: {
    copy (a, b) {
      this.calls ++
      if (a.length !== b.length) return this.onChange()
      for (let i=0; i<a.length; i++) {
        if (a[i] !== b[i]) return this.onChange()
      }
    }
  },
  methods: {
    onChange () {
      console.log('change')
      this.changes ++
    },
    addItem () { this.list.push({a: 0}) },
    incrItem (i) { this.list[i].a ++ },
    removeItem(i) { this.list.splice(i, 1) }
  }
}

https://jsfiddle.net/aurelienlt89/x2kca57e/15/

The idea is to build a computed value copy that has exactly what we want to check. Computed values are magic and only put watchers on the properties that were actually read (here, the items of list read in list.slice()). The checks in the copy watcher are actually almost useless (except weird corner cases maybe) because computed values are already extremely precise.

Upvotes: 9

pespantelis
pespantelis

Reputation: 15372

You should pass an object instead of boolean as options, so:

mounted: function () {
  this.$watch('things', function () {
    console.log('a thing changed')
  }, {deep:true})
}

Or you could set the watcher into the vue instance like this:

new Vue({
  ...
  watch: {
    things: {
      handler: function (val, oldVal) {
        console.log('a thing changed')
      },
      deep: true
    }
  },
  ...
})

[demo]

Upvotes: 92

Related Questions