Joe82
Joe82

Reputation: 1251

Vue - Deep watch change of array of objects, either if a object is added of modified

I'm trying to create an element in vue.js, so that when I update my cart it will show a warning with the item added/updated to cart. So if I add a new car, it would show that last car added.

cars: [
  { name: 'Porsche', quantity: 2},
  { name: 'Ferrari', quantity: 1},
  { name: 'Toyota', quantity: 3}
]

to

cars: [
  { name: 'Porsche', quantity: 2},
  { name: 'Ferrari', quantity: 1},
  { name: 'Toyota', quantity: 3},
  { name: 'Mustang', quantity: 1}
]

will show

<div>
You have 1 x Mustang in Cart
</div>

But if I update the quantity of a car that was already in the cart, it will show that last car updated.

cars: [
  { name: 'Porsche', quantity: 2},
  { name: 'Ferrari', quantity: 1},
  { name: 'Toyota', quantity: 3}
]

to

cars: [
  { name: 'Porsche', quantity: 2},
  { name: 'Ferrari', quantity: 1},
  { name: 'Toyota', quantity: 4}
]

will show

<div>
You have 4 x Toyota in Cart
</div>

So far I made it work based in this answer

new Vue({
el: '#app',
data: {
    cars: [
      { name: 'Porsche', quantity: 2},
      { name: 'Ferrari', quantity: 1},
      { name: 'Toyota', quantity: 3}
    ]
}
});

Vue.component('car-component', {
props: ["car"],
data: function() {
  return {
    lastAdded:''
  }
},
template: `
  <div>
    You have {{lastAdded.quantity}} x {{lastAdded.name}} in Cart
  </div>`,

watch: {
  car: {
    handler: function(newValue) {
       this.lastAdded = newValue;
    },
      deep: true
  }
}
});

html

<script src="https://unpkg.com/[email protected]/dist/vue.js"></script>
<body>
  <div id="app">
    <p>Added to Cart:</p>
    <car-component :car="car" v-for="car in cars"></car-component>
  </div>
</body>

The point is that now it just detects when a object is already in the cart and changes quantity, but not when there is a new car added. I tried to play with another watcher, but it didn't work. Thanks in advance!

Upvotes: 1

Views: 3759

Answers (2)

tony19
tony19

Reputation: 138196

You could pass the entire cars[] array to <car-component>, and allow the component to determine which element of cars[] to display a message about:

  1. In car-component, add a prop (typed for safety) to hold the passed-in cars[]:

    Vue.component('car-component', { // ... props: { cars: Array }, }

  2. Add two data properties:

* `car` - the current car.
* `copyOfCars` - the last known copy of `cars[]`, used to determine which array element has changed. *Note: While watchers are provided both the old and new values of the watched property, the old value does not actually indicate the previous value for arrays of objects.*

        Vue.component('car-component', {
          //...
          data() {
            return {
              car: {},
              copyOfCars: undefined, // `undefined` because we don't need it to be reactive
            };
          },
        }
  1. Define a method (e.g., named findActiveCar) that determines which element in a given cars[] is most recently "active" (newly added or modified).

    Vue.component('car-component', { // ... methods: { /** * Gets the newest/modified car from the given cars */ findActiveCar(newCars) { if (!newCars || newCars.length === 0) return {};

         let oldCars = this.copyOfCars;
    
         // Assume the last item of `newCars` is the most recently active
         let car = newCars[newCars.length - 1];
    
         // Search `newCars` for a car that doesn't match its last copy in `oldCars`
         if (oldCars) {
           for (let i = 0; i < Math.min(newCars.length, oldCars.length); i++) {
             if (newCars[i].name !== oldCars[i].name
                 || newCars[i].quantity !== oldCars[i].quantity) {
               car = newCars[i];
               break;
             }
           }
         }
    
         this.copyOfCars = JSON.parse(JSON.stringify(newCars));
         return car;
       }
     }
    

    }

  2. Define a watcher on the cars property that sets car to the new/modified item from findActiveCar().

    Vue.component('car-component', { // ... watch: { cars: { handler(newCars) { this.car = this.findActiveCar(newCars); }, deep: true, // watch subproperties of array elements immediate: true, // run watcher immediately on this.cars[] } }, }

Vue.component('car-component', {
  props: {
    cars: Array,
  },
  data() {
    return {
      car: {},
      copyOfCars: undefined,
    }
  },
  template: `<div>You have {{car.quantity}} x {{car.name}} in Cart</div>`,

  watch: {
    cars: {
      handler(newCars) {
        this.car = this.findActiveCar(newCars);
      },
      deep: true,
      immediate: true,
    }
  },

  methods: {
    findActiveCar(newCars) {
      if (!newCars || newCars.length === 0) return {};

      let oldCars = this.copyOfCars;
      let car = newCars[newCars.length - 1];

      if (oldCars) {
        for (let i = 0; i < Math.min(newCars.length, oldCars.length); i++) {
          if (newCars[i].name !== oldCars[i].name
              || newCars[i].quantity !== oldCars[i].quantity) {
            car = newCars[i];
            break;
          }
        }
      }

      this.copyOfCars = JSON.parse(JSON.stringify(newCars));
      return car;
    }
  }
});


new Vue({
  el: '#app',
  data: () => ({
    cars: [
      { name: 'Porsche', quantity: 2},
      { name: 'Ferrari', quantity: 1},
      { name: 'Toyota', quantity: 3}
    ]
  }),
  methods: {
    addCar() {
      this.cars.push({
        name: 'Mustang', quantity: 1
      })
    }
  }
})
<script src="https://unpkg.com/[email protected]"></script>

<div id="app">
  <h1>Added to Cart</h1>
  <button @click="addCar">Add car</button>
  <ul>
    <li v-for="(car, index) in cars" :key="car.name + index">
      <span>{{car.name}} ({{car.quantity}})</span>
      <button @click="car.quantity++">+</button>
    </li>
  </ul>
  <car-component :cars="cars" />
</div>

Upvotes: 1

RobotOptimist
RobotOptimist

Reputation: 376

hmm how would I do this?

seems to me we have an array of objects and we are tracking the most recently added or modified object. Sure.

So, I think I'd want to only track the recently modified object and render that.

first the html:

<div id="app">
    <p>Added to Cart:</p>
    <car-component :car="latestCar"></car-component>
</div>

and the vue instance:

new Vue({
el: '#app',
data: {
    cars: [
      { name: 'Porsche', quantity: 2},
      { name: 'Ferrari', quantity: 1},
      { name: 'Toyota', quantity: 3}
    ],
    latestCar: {}
},
methods: {
  updateLatestCar(car) {
     this.latestCar = car;
     //call this method from any other method where updates take place
     //so if would be called from your addCar method and your updateCar method
     //(which I assume exist even though they are not shown in your code)        
  }
}
});

Vue.component('car-component', {
props: ["car"],
data: function() {
  return {
    lastAdded:''
  }
},
template: `
  <div>
    You have {{lastAdded.quantity}} x {{lastAdded.name}} in Cart
  </div>`,

watch: {
  car: {
    handler: function(newValue) {
       this.lastAdded = newValue;
    },
      deep: true
  }
}
});

If you are modifying your array of objects via some method that is external to the Vue instance then that will require some additional thought.

But it seems like for this you'd have some methods in the Vue instance methods block like this:

addCar(car) {
  this.cars.push(car);
  this.updateLatestCar(car);
},
updateCar(index, car) {
  this.cars[index] = car;
  this.updateLatestCar(car);
}

Upvotes: 2

Related Questions