118
votes

I have an array called people that contains objects as follows:

Before

[
  {id: 0, name: 'Bob', age: 27},
  {id: 1, name: 'Frank', age: 32},
  {id: 2, name: 'Joe', age: 38}
]

It can change:

After

[
  {id: 0, name: 'Bob', age: 27},
  {id: 1, name: 'Frank', age: 33},
  {id: 2, name: 'Joe', age: 38}
]

Notice Frank just turned 33.

I have an app where I am trying to watch the people array and when any of the values changes then log the change:

<style>
input {
  display: block;
}
</style>

<div id="app">
  <input type="text" v-for="(person, index) in people" v-model="people[index].age" />
</div>

<script>
new Vue({
  el: '#app',
  data: {
    people: [
      {id: 0, name: 'Bob', age: 27},
      {id: 1, name: 'Frank', age: 32},
      {id: 2, name: 'Joe', age: 38}
    ]
  },
  watch: {
    people: {
      handler: function (val, oldVal) {
        // Return the object that changed
        var changed = val.filter( function( p, idx ) {
          return Object.keys(p).some( function( prop ) {
            return p[prop] !== oldVal[idx][prop];
          })
        })
        // Log it
        console.log(changed)
      },
      deep: true
    }
  }
})
</script>

I based this on the question that I asked yesterday about array comparisons and selected the quickest working answer.

So, at this point I expect to see a result of: { id: 1, name: 'Frank', age: 33 }

But all I get back in the console is (bearing in mind i had it in a component):

[Vue warn]: Error in watcher "people" 
(found in anonymous component - use the "name" option for better debugging messages.)

And in the codepen that I made, the result is an empty array and not the changed object that changed which would be what I expected.

If anyone could suggest why this is happening or where I have gone wrong here then it would be greatly appreciated, many thanks!

6
my only problem with all the solutions below is that they fire on each change, in my case i want to make a database API call to update changed items only. an input v-model will fire 100s of times, i want to record the changes as they happen but save them to database when user clicks apply changes, how would you go about thatPirateApp
@PirateApp Not sure I follow your question exactly but perhaps this solves the issue that you're having: vuejs.org/v2/guide/forms.html#lazy. You could also opt to take deeper control over what happens by instead using the select / change events to trigger your logic? It's hard to say without an example, make a new question with come code and post a link, i'll try to help to solve it.Craig van Tonder

6 Answers

152
votes

Your comparison function between old value and new value is having some issue. It is better not to complicate things so much, as it will increase your debugging effort later. You should keep it simple.

The best way is to create a person-component and watch every person separately inside its own component, as shown below:

<person-component :person="person" v-for="person in people"></person-component>

Please find below a working example for watching inside person component. If you want to handle it on parent side, you may use $emit to send an event upwards, containing the id of modified person.

Vue.component('person-component', {
    props: ["person"],
    template: `
        <div class="person">
            {{person.name}}
            <input type='text' v-model='person.age'/>
        </div>`,
    watch: {
        person: {
            handler: function(newValue) {
                console.log("Person with ID:" + newValue.id + " modified")
                console.log("New age: " + newValue.age)
            },
            deep: true
        }
    }
});

new Vue({
    el: '#app',
    data: {
        people: [
          {id: 0, name: 'Bob', age: 27},
          {id: 1, name: 'Frank', age: 32},
          {id: 2, name: 'Joe', age: 38}
        ]
    }
});
<script src="https://unpkg.com/vue@2.1.5/dist/vue.js"></script>
<body>
    <div id="app">
        <p>List of people:</p>
        <person-component :person="person" v-for="person in people"></person-component>
    </div>
</body>
22
votes

I have changed the implementation of it to get your problem solved, I made an object to track the old changes and compare it with that. You can use it to solve your issue.

Here I created a method, in which the old value will be stored in a separate variable and, which then will be used in a watch.

new Vue({
  methods: {
    setValue: function() {
      this.$data.oldPeople = _.cloneDeep(this.$data.people);
    },
  },
  mounted() {
    this.setValue();
  },
  el: '#app',
  data: {
    people: [
      {id: 0, name: 'Bob', age: 27},
      {id: 1, name: 'Frank', age: 32},
      {id: 2, name: 'Joe', age: 38}
    ],
    oldPeople: []
  },
  watch: {
    people: {
      handler: function (after, before) {
        // Return the object that changed
        var vm = this;
        let changed = after.filter( function( p, idx ) {
          return Object.keys(p).some( function( prop ) {
            return p[prop] !== vm.$data.oldPeople[idx][prop];
          })
        })
        // Log it
        vm.setValue();
        console.log(changed)
      },
      deep: true,
    }
  }
})

See the updated codepen

20
votes

It is well defined behaviour. You cannot get the old value for a mutated object. That's because both the newVal and oldVal refer to the same object. Vue will not keep an old copy of an object that you mutated.

Had you replaced the object with another one, Vue would have provided you with correct references.

Read the Note section in the docs. (vm.$watch)

More on this here and here.

9
votes

The component solution and deep-clone solution have their advantages, but also have issues:

  1. Sometimes you want to track changes in abstract data - it doesn't always make sense to build components around that data.

  2. Deep-cloning your entire data structure every time you make a change can be very expensive.

I think there's a better way. 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) {
      // Handle changes here!
      console.log(newVal);
    },
  },
  created () {
    this.list.forEach((val) => {
      this.$watch(() => val, this.handleChange, {deep: true});
    });
  },
});

With this structure, handleChange() will receive the specific list item that changed - from there you can do any handling you like.

I have also documented a more complex scenario here, in case you are adding/removing items to your list (rather than only manipulating the items already there).

6
votes

This is what I use to deep watch an object. My requirement was watching the child fields of the object.

new Vue({
    el: "#myElement",
    data:{
        entity: {
            properties: []
        }
    },
    watch:{
        'entity.properties': {
            handler: function (after, before) {
                // Changes detected.    
            },
            deep: true
        }
    }
});
0
votes

if we have Object or Array of object and we want to watch them in Vuejs or NUXTJS need to use deep: true in watch

watch: {
  'Object.key': {
     handler (val) {
         console.log(val)
     },
     deep: true
   } 
 }

 watch: {
  array: {
     handler (val) {
         console.log(val)
     },
     deep: true
   } 
 }