No Data Duplication! Fancy Computed Prop
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.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeThis cool new completeItems
array combines data from two AJAX calls: one for the cart
and another for the products that are in that cart. If we could make this available to our template, we could loop over it and start printing out real product data.
// ... lines 1 - 28 | |
<script> | |
// ... lines 30 - 34 | |
export default { | |
name: 'ShoppingCart', | |
// ... lines 37 - 41 | |
watch: { | |
async cart() { | |
// ... lines 44 - 48 | |
const completeItems = this.cart.items.map((cartItem) => ( | |
{ | |
product: products.find((product) => product['@id'] === cartItem.product), | |
color: cartItem.color, | |
quantity: cartItem.quantity, | |
} | |
)); | |
console.log(completeItems); | |
}, | |
}, | |
}; | |
</script> | |
// ... lines 61 - 71 |
So... how can we do that? Easy! Create a new completeItems
data, set it here, then reference it in the template! We're unstoppable!
Be Careful with Duplicate Data
But... there's a problem with that approach, and I bet some of you see it too. What is it? Duplicated data. Duh, duh, duh!
If we set completeItems
onto data, then some of the basic cart item data - the product IRI, color IRI and quantity - would be duplicated in two places. They would be reflected in completeItems
... but they would also be on the cart
data itself.
And we never want to store a piece of data in multiple places. Because, if we later changed a piece of data - like the quantity
of an item - it would change in one spot... but not the other... unless we added extra code to keep them in sync. Yuck. It's really no different than a database: you typically don't want to store a piece of data in multiple places because they could get out of sync.
Computed Prop to the Rescue
So... let's be smarter. Think about it: the only new piece of data we have is the products
array that we get back from the AJAX call. If we stored that as data... we could still access this nice completeItems
array in the template via a computed property.
Let's do it! Start by adding a data
key, which is a function, then returning an object with products
initialized to null
.
// ... lines 1 - 28 | |
<script> | |
// ... lines 30 - 34 | |
export default { | |
name: 'ShoppingCart', | |
// ... lines 37 - 41 | |
data() { | |
return { | |
products: null, | |
}; | |
}, | |
// ... lines 47 - 63 | |
}; | |
</script> | |
// ... lines 66 - 76 |
Down below in the watcher function, instead of const products
, say this.products
... and reference this.products
below in the loop.
// ... lines 1 - 28 | |
<script> | |
// ... lines 30 - 34 | |
export default { | |
name: 'ShoppingCart', | |
// ... lines 37 - 41 | |
data() { | |
return { | |
products: null, | |
}; | |
}, | |
watch: { | |
async cart() { | |
// ... lines 49 - 51 | |
this.products = productsResponse.data['hydra:member']; | |
// ... line 53 | |
const completeItems = this.cart.items.map((cartItem) => ( | |
{ | |
product: this.products.find((product) => product['@id'] === cartItem.product), | |
// ... lines 57 - 58 | |
} | |
)); | |
console.log(completeItems); | |
}, | |
}, | |
}; | |
</script> | |
// ... lines 66 - 76 |
Next, add a computed
key with one new computed prop. Call it completeCart()
.
// ... lines 1 - 28 | |
<script> | |
// ... lines 30 - 34 | |
export default { | |
name: 'ShoppingCart', | |
// ... lines 37 - 46 | |
computed: { | |
completeCart() { | |
// ... lines 49 - 63 | |
}, | |
}, | |
// ... lines 66 - 73 | |
}; | |
</script> | |
// ... lines 76 - 86 |
Before we do anything else in this function, if someone calls us and the cart
data is not ready yet, we should also return null. So if !this.cart
... or if !this.products
- if we also haven't finished loading the products - then return null
. This means that if completeCart
returns null, things are still loading.
// ... lines 1 - 28 | |
<script> | |
// ... lines 30 - 34 | |
export default { | |
name: 'ShoppingCart', | |
// ... lines 37 - 46 | |
computed: { | |
completeCart() { | |
if (!this.cart || !this.products) { | |
return null; | |
} | |
// ... lines 52 - 63 | |
}, | |
}, | |
// ... lines 66 - 73 | |
}; | |
</script> | |
// ... lines 76 - 86 |
Now, copy all this completeItems
stuff from watch
, move it here, but instead of logging, return a new object. We'll make this look just like the cart... just for consistency: with an items
key set to completeItems
.
// ... lines 1 - 28 | |
<script> | |
// ... lines 30 - 34 | |
export default { | |
name: 'ShoppingCart', | |
// ... lines 37 - 46 | |
computed: { | |
completeCart() { | |
if (!this.cart || !this.products) { | |
return null; | |
} | |
const completeItems = this.cart.items.map((cartItem) => ( | |
{ | |
product: this.products.find((product) => product['@id'] === cartItem.product), | |
color: cartItem.color, | |
quantity: cartItem.quantity, | |
} | |
)); | |
return { | |
items: completeItems, | |
}; | |
}, | |
}, | |
// ... lines 66 - 73 | |
}; | |
</script> | |
// ... lines 76 - 86 |
That should do it! If we've done everything correctly, after the cart
data has finished loading, the watcher will call our function, we will make an AJAX request for the products
, and then, finally, in our template, we will reference the completeCart
variable, which will combine all that data once it's available. Remember: one of the magic things about computed properties is that Vue will automatically re-render and re-call our computed function whenever any piece of data that it references - like this.cart
or this.products
- changes.
Rendering Complete Cart Data
So let's go to our template. We basically want to update everything from cart
to completeCart
. Copy that, use it on the v-if
and inside the v-for
.
Then, since cartItem.product
will now be an object, we can prove everything works by printing cartItem.product.name
. Oh, and I'll change one more spot to completeCart
.
<template> | |
<div :class="[$style.component, 'container-fluid']"> | |
<div class="row"> | |
// ... lines 4 - 5 | |
<div class="col-xs-12 col-lg-9"> | |
// ... lines 7 - 8 | |
<div class="content p-3"> | |
<loading v-show="completeCart === null" /> | |
<div v-if="completeCart !== null"> | |
<div | |
v-for="(cartItem, index) in completeCart.items" | |
:key="index" | |
> | |
{{ cartItem.product.name }} ({{ cartItem.quantity }}) | |
</div> | |
<div v-if="completeCart.items.length === 0"> | |
Your cart is empty! Get to shopping! | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
</template> | |
// ... lines 28 - 86 |
Testing time! Back at the browser... ha! I didn't even need to refresh! It's already printing the product name. I know, it doesn't look that impressive yet, but we did just put together a lot of data.
Check out the Vue dev tools for this component: cart
data, products
data and a beautiful completeCart
computed prop that allows us to easily use the data we need in the template, without duplicating anything.
Next: we're still missing one piece of data inside completeCart
: the color. This is still an IRI string... but what we need is the color data, which will include the hex color so that we can render a color box on the screen. Let's finish that and learn a cool trick for making AJAX requests in parallel.
Thank you for this formation, however could you do an episode on translation in Vue.js?