1

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/vue@2.5.17/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!

Joe82
  • 1,357
  • 2
  • 24
  • 40
  • Won't this show the warning message for each car? I thought you only wanted it to be shown for the last item added/updated in the cart. – Decade Moon Sep 13 '18 at 13:04
  • Vue has some caveats to look out for when tracking objects and arrays. Check this out: https://vuejs.org/v2/guide/list.html#Caveats – Kwesi Smart Sep 13 '18 at 13:08
  • @DecadeMoon I edited the html, it should show lastItem values – Joe82 Sep 13 '18 at 13:13
  • @Joe82 What I mean is, you are repeating `` for each car, and `` will render "You have ... in Cart", so you're going to have multiple messages. I'm confused because your question suggests that you only want one message displayed for the last item, not for each item. – Decade Moon Sep 13 '18 at 13:16
  • @Joe82 Otherwise if your code is correct, you don't need deep watching and you needn't do what you're trying to do. – Decade Moon Sep 13 '18 at 13:18
  • @DecadeMoon Yep, I just want to display the last item that either was added or modified (now it just shows the last one that was modified). I followed that pattern as in the other answer it was voted as the optimal for vue, but tbh I'm not sure of what the advantages are with that approach. – Joe82 Sep 13 '18 at 13:20

2 Answers2

2

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);
}
RobotOptimist
  • 346
  • 3
  • 8
  • Thanks for the answer! I thought about something like this, but I'd rather solve it with watchers. The point is that irl I have several methods that update the array of objects, which is in stored in vuex, and having to add this update method to each one of them doesn't seem the optimal solution. – Joe82 Sep 13 '18 at 16:56
1

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/vue@2.5.17"></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>
tony19
  • 125,647
  • 18
  • 229
  • 307
  • Works great! Just one question: what does the `props: {cars: Array}` part do, and what is the different from `props:['cars']`? Thanks! – Joe82 Sep 17 '18 at 16:28
  • 1
    Vue allows you to [specify the expected type of a prop](https://vuejs.org/v2/guide/components-props.html#Prop-Types). So, `props: {cars: Array}` tells Vue to emit an error if the provided `cars` is not actually an array. On the other hand with `props: ['cars']`, there is no type checking, and `cars` can be anything. – tony19 Sep 17 '18 at 16:31