Emitting up the Component Tree

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $12.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

I'm really happy with how our cart page is looking. Now we need to bring the quantity and remove button to life and make them work. First up is the quantity input.

As a reminder, each row is being rendered by the ShoppingCartItem component... but the data for quantity lives up on ShoppingCart... inside the cart data: each item has, among other things, a quantity. So this is what we ultimately need to update.

The input lives down here in the ShoppingCartItem component. So, to communicate up to ShoppingCart, we need to emit an event. No problem!

Emitting the Event

Over in cart-item, find the input and add @input="". So, whenever this value changes, call a new function: updateQuantity:

<template>
<div :class="[$style.component, 'row', 'p-3']">
... lines 3 - 15
<div class="col-3">
<input
... lines 18 - 21
@input="updateQuantity"
/>
</div>
... lines 25 - 34
</div>
</template>
... lines 37 - 87

Copy that, scroll to the bottom and... add a methods key with an updateQuantity function inside. This will be passed an event object.

... lines 1 - 37
<script>
... lines 39 - 40
export default {
name: 'ShoppingCartItem',
... lines 43 - 56
methods: {
updateQuantity(event) {
... lines 59 - 63
},
},
};
</script>
... lines 68 - 87

All we need to do here is emit an event. Do that with this.$emit() and... call the event, how about, updateQuantity. Though, I have to admit, I got a little lazy when I recorded this. Technically this event name is fine and will work.. but the Vue best practice is to use kebab-casing for event names. So this should be update-quantity. Both work, but follow the convention... not lazy Ryan.

... lines 1 - 37
<script>
... lines 39 - 40
export default {
name: 'ShoppingCartItem',
... lines 43 - 56
methods: {
updateQuantity(event) {
this.$emit('updateQuantity', {
... lines 60 - 62
});
},
},
};
</script>
... lines 68 - 87

Anyways, for the event data, let's think. We really need to pass 2 things: something that identifies which cart item is being updated and what its new quantity should be.

For the first thing, the way we're identifying each item in the cart is via its product and color combination. So pass both: productId set to this.item.product['@id'], then colorId set to basically the same thing... but first check to see if there is a color. If there is, pass this.item.color['@id'], else pass null:

... lines 1 - 37
<script>
... lines 39 - 40
export default {
name: 'ShoppingCartItem',
... lines 43 - 56
methods: {
updateQuantity(event) {
this.$emit('updateQuantity', {
productId: this.item.product['@id'],
colorId: this.item.color ? this.item.color['@id'] : null,
... line 62
});
},
},
};
</script>
... lines 68 - 87

You could also pass that id property we added to each item... and then find the correct item using that... it's your call.

The other thing we need to pass is the quantity, which isn't stored as a piece data, but is available via event.target.value:

... lines 1 - 37
<script>
... lines 39 - 40
export default {
name: 'ShoppingCartItem',
... lines 43 - 56
methods: {
updateQuantity(event) {
this.$emit('updateQuantity', {
productId: this.item.product['@id'],
colorId: this.item.color ? this.item.color['@id'] : null,
quantity: event.target.value,
});
},
},
};
</script>
... lines 68 - 87

Beautiful! Let's check this in the browser. Go to the Vue dev tools: we can at least make sure the event emits correctly. And... perfect! When we click up or down, the updateQuantity event does show up.

Converting the Quantity to a Number

Check the "payload".. this contains the event object. Oh... that's not quite right: the color and product id's look good... but the quantity is a string. That makes sense: input values are strings... but we want a number.

This is exactly the reason why we sometimes use v-model.number: it grabs the value, converts it into a number and then updates the data. We're not using v-model here, but we can do that same conversion manually. Wrap the input value with parseFloat():

... lines 1 - 37
<script>
... lines 39 - 40
export default {
name: 'ShoppingCartItem',
... lines 43 - 56
methods: {
updateQuantity(event) {
this.$emit('updateQuantity', {
... lines 60 - 61
quantity: parseFloat(event.target.value),
});
},
},
};
</script>
... lines 68 - 87

Now when we change the quantity... yes! We have a number!

Emitting the Event up the Component Chain

So: unfortunately, we can't just listen to the new updateQuantity event from shopping-cart... because shopping-cart doesn't render cart-item directly: it's rendered by the shopping cart list component.

But it's no problem! We just need to do one extra step: listen to the event in ShoppingCartList and re-emit that event so that we can then listen to it from the top-level ShoppingCart.

Open index.vue and find where we render shopping-cart-item: here it is. Add @updateQuantity to listen to the new event. Normally, we set this to a method name... but we can also just... write code here! Re-emit the event with $emit()... and I'll keep the same name - updateQuantity. For the data... just use the same event! We have access to it here as $event:

<template>
<div>
... lines 3 - 6
<div v-if="items.length">
... lines 8 - 20
<shopping-cart-item
... lines 22 - 24
@updateQuantity="$emit('updateQuantity', $event)"
/>
... lines 27 - 30
</div>
</div>
</template>
... lines 34 - 64

Check this out in the Vue dev tools: now when we change the quantity... yes! There are two events, one emitted from each component.

Finally, we can listen to this event in ShoppingCart and use it to update the data and save the new quantity back to the server. Let's do that next!

Leave a comment!

This course is also built to work with Vue 3!

What JavaScript libraries does this tutorial use?

// package.json
{
    "devDependencies": {
        "@fortawesome/fontawesome-free": "^5.15.1", // 5.15.1
        "@symfony/webpack-encore": "^0.30.0", // 0.30.2
        "axios": "^0.19.2", // 0.19.2
        "bootstrap": "^4.4.1", // 4.5.3
        "core-js": "^3.0.0", // 3.6.5
        "eslint": "^6.7.2", // 6.8.0
        "eslint-config-airbnb-base": "^14.0.0", // 14.2.0
        "eslint-plugin-import": "^2.19.1", // 2.22.1
        "eslint-plugin-vue": "^6.0.1", // 6.2.2
        "regenerator-runtime": "^0.13.2", // 0.13.7
        "sass": "^1.29.0", // 1.29.0
        "sass-loader": "^8.0.0", // 8.0.2
        "vue": "^2.6.11", // 2.6.12
        "vue-loader": "^15.9.1", // 15.9.4
        "vue-template-compiler": "^2.6.11", // 2.6.12
        "webpack-notifier": "^1.6.0" // 1.8.0
    }
}