How to efficiently monitor and calculate changes in an array of objects using Vue?

I have a collection named people that holds data in the form of objects:

Previous Data

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

This data can be modified:

New Data

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

Note that Frank has now become 33 years old.

I am working on an application where I want to monitor changes in the people array and log those changes when they occur:

<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) {
        // Identify the changed object
        var changed = val.filter( function( p, idx ) {
          return Object.keys(p).some( function( prop ) {
            return p[prop] !== oldVal[idx][prop];
          })
        })
        // Log the change
        console.log(changed)
      },
      deep: true
    }
  }
})
</script>

I referred to a recent question about comparing arrays and implemented the solution that worked fastest.

Therefore, I anticipate seeing the output:

{ id: 1, name: 'Frank', age: 33 }

However, instead of getting the expected result, all I see in the console is (keep in mind this was within a component):

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

In the codepen link that I created, the output is an empty array rather than the specific object that underwent the change, which is not what I had anticipated.

If anyone could provide insight into why this might be happening or point out any mistakes I made, I would greatly appreciate it. Thank you!

Answer №1

It seems like there may be an issue with your comparison function between the old value and the new value. Simplifying things will make debugging easier in the future. Keeping it simple is key.

Consider creating a person-component to watch each person individually within its component, as shown below:

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

Here is an example of how to watch inside the person component. If you prefer handling it on the parent side, you can use $emit to send an event upwards with the modified person's id.

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 + " has been 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/<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="f1878494a1938e819ae1">[email protected]</a>/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>

Answer №2

To resolve your issue, I have restructured the implementation by creating an object to track previous changes and compare them. This method can be utilized to address your problem effectively.

I have devised a new approach where the original value is stored in a separate variable and then utilized within a watch function.

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) {
        // Identify the changed object
        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 any changes
        vm.setValue();
        console.log(changed)
      },
      deep: true,
    }
  }
})

Check out the revised codepen example

Answer №3

It is considered well-defined behavior. Retrieving the old value for a mutated object is not possible because both the newVal and oldVal point to the same object. Vue does not retain an old copy of an object that has been mutated.

If you had replaced the object with a new one, Vue would have given you accurate references.

Refer to the Note section in the documentation (vm.$watch)

For more information, check here and here.

Answer №4

Although the component solution and deep-clone solution offer their own benefits, they also come with drawbacks:

  1. At times, monitoring changes in abstract data may not align with building components for that data.

  2. Performing a deep clone of your entire data structure every time you make a change can be prohibitively costly.

There is a more efficient approach I propose. If you wish to track all items in a list and identify precisely which item in the list was modified, you can establish custom watchers for each individual item, like this:

var vm = new Vue({
  data: {
    list: [
      {name: 'obj1 to watch'},
      {name: 'obj2 to watch'},
    ],
  },
  methods: {
    handleChange (newVal) {
      // Implement handling logic here!
      console.log(newVal);
    },
  },
  created () {
    this.list.forEach((val) => {
      this.$watch(() => val, this.handleChange, {deep: true});
    });
  },
});

With this setup, handleChange() will receive the specific altered list item, allowing you to perform any required actions accordingly.

I have also provided guidance on a more intricate scenario here, specifically if you are adding/removing elements from your list rather than solely modifying existing items.

Answer №5

This is the method I use for closely monitoring an object's properties. I needed to keep a close eye on all child fields within the object.

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

Answer №6

If you have an Object or Array of objects that you want to keep an eye on in Vuejs, you will need to include deep: true in the watch.


watch: {
  'Object.key': {
    handler (after, before) {
      // Changes detected.
    },
    deep: true
  }
}

watch: {
  array: {
    handler (after, before) {
      // Changes detected.
    },
    deep: true
  }
}

Answer №7

Instead of using the "watch" method, I opted for the "computed" method to solve the problem!

I haven't run this code yet, but I believe it should function properly. Please let me know in the comments if it doesn't.

<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}
    ],
    oldVal: {},
    peopleComputed: computed({
      get(){
        this.$data.oldVal = { ...people };
        return people;
      },
      set(val){
        // Identify and return the object that has changed
        var changed = val.filter( function( p, idx ) {
          return Object.keys(p).some( function( prop ) {
            return p[prop] !== this.$data.oldVal[idx][prop];
          })
        })
        // Output the changes for logging purposes
        console.log(changed)
        this.$data.people = val;
      }
    }),
  }
})
</script>

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

What could be causing my data to not appear in a new row?

Currently, I am facing an issue with how checked checkboxes are displayed. Is there a way to have each checkedbox appear on a new line instead of stacking beside each other in the same line? b-modal#modal-1.d-flex(title='Dodaj leki' hide-footer) ...

Avoid clicking on the HTML element based on the variable's current value

Within my component, I have a clickable div that triggers a function called todo when the div is clicked: <div @click="todo()"></div> In addition, there is a global variable in this component named price. I am looking to make the af ...

Using jQuery to validate the existence of a link

Within my pagination div, I have links to the previous page and next page. The link to the previous page is structured as follows: <span id="previous"><a href="www.site.com/page/1" >Previous</a>. However, on the first page, there will be ...

ReferenceError: 'exports' is undefined in the context of Typescript Jest

I'm currently delving into unit testing with jest and encountered an error that looks like this: > npm run unit > <a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="771f181012374659475947">[email protected]</ ...

Getting a specific value from a REST API array in JavaScript: Tips and tricks

After being stuck on this problem for 8 hours, I finally decided to seek help: In my JavaScript file, I am attempting to extract data from a REST API response. The response consists of an array of objects structured like this: [{"start":"2017-04-21 14:40 ...

Is it necessary to close the browser for jQuery to reload an XML document?

I've successfully used jQuery's $.ajax to retrieve an xml value in my code. However, I'm facing an issue where any changes made to the xml document are not reflected upon a browser refresh. Specifically, the new saved xml value xmlImagePath ...

Sorting alphanumeric strings in React Bootstrap Table Next on a dynamic table

I am facing an issue with sorting columns in a dynamic table with over 70 columns using React-Bootstrap-Table-Next. The problem arises when trying to sort the columns in alphanumerical order, as some columns contain numbers and others contain letters. The ...

Issues with CSS Modules not applying styles in next.js 13 version

Employing next.js 13.1.1 along with /app Previously, I had been handling all of my styles using a global.css, however, I am now attempting to transition them into CSS Modules. Within my root layout.js, there is a Header component that is imported from ./ ...

I could use some assistance with deciphering JSON data

After using console.log to display the data I received, I observed an object structured as follows (I trimmed some details for clarity and used ... to indicate repetitive information): [ Submission { title: 'Untitled', content: { ur ...

Learn how to manipulate Lit-Element TypeScript property decorators by extracting values from index.html custom elements

I've been having some trouble trying to override a predefined property in lit-element. Using Typescript, I set the value of the property using a decorator in the custom element, but when I attempt to override it by setting a different attribute in the ...

Choose the radio option with a personalized tag

I'm dealing with an array of colors and I want to use them to select text colors using radio buttons. I have customized the default radio button using a label, but now I'm facing an issue where the radio button only selects one color while the la ...

Shuffle a Document Fragment in a random order before adding it to the DOM using JavaScript

My JavaScript code is generating a text area and button dynamically. I have successfully implemented it so that when a value is entered into the text area and the button is clicked, a random number of SPAN tags are created. Each character from the input va ...

Close to completing the AngularJS filter using an array of strings

I'm currently working on developing a customized angular filter that will be based on an array of strings. For instance: $scope.idArray = ['1195986','1195987','1195988'] The data that I aim to filter is structured as fo ...

How can I retrieve objects using the JSON function?

I need to design a function that returns objects like the following: function customFunction(){ new somewhere.api({ var fullAddress = ''; (process address using API) (return JSON data) })open(); } <input type= ...

How can I automatically close the menu when I click on a link in Vue?

Tap the menu icon Select a link The URL changes in the background. However, the menu remains open. How do I close the menu when a link is selected? The menu is wrapped in a details HTML element. Is there a way to remove the "open" attribute from the detai ...

eliminating labels from a string through recursive method

One of my challenges involves developing a function that can remove tags from an input string. For example: '<strong>hello <em>my name <strong>is</strong> </em></strong>' The desired result should be: &apos ...

Can you explain how I can declare a variable to store a scraped element in Puppeteer?

const puppeteer = require('puppeteer'); (async () => { const browser = await puppeteer.launch({ headless: false, defaultViewport: null }) const page = await browser.newPage() await page.goto('https://www.supre ...

Steer clear of receiving null values from asynchronous requests running in the background

When a user logs in, I have a request that retrieves a large dataset which takes around 15 seconds to return. My goal is to make this request upon login so that when the user navigates to the page where this data is loaded, they can either see it instantly ...

In order to display the new component upon the first click in React, my button requires a double click

I have a project that utilizes the open Trivia API to fetch data. I've completed the development and everything appears to be working well so far. However, there's a bug where upon initially rendering the app, the first time I click the button to ...

Setting maxFontSizeMultiplier for all Text components

Is there a way to apply the prop maxFontSizeMultiplier={1} to all instances of <Text/> in my app without the need for a custom component? ...