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 SubscribeWe now have all the cart data we need to really render the products! For each item, we're going to print out quite a few details, like name, price, a quantity box and a "remove from cart" button. To keep things organized, let's make a new component whose job is to render one item.
In the assets/components/
directory, add a new folder called shopping-cart
. This time, to create the component, I'll do something a bit different: right click on shopping-cart
, search for "Vue" and select "Vue component". Call it cart-item
.
<template> | |
</template> | |
<script> | |
export default { | |
name: "cart-item.vue" | |
} | |
</script> | |
<style scoped> | |
</style> |
Ok! This generates a basic Vue structure... though it looks a bit different than how we normally do things. PhpStorm does let you customize these templates, if you want to improve this.
Let's clean things up. Start by adding a div with a class
attribute. Actually, make it dynamic - :class
set to an array - so we can reference a modular class - $style.component
- and give it two other classes. We'll add the component
style in a minute.
<template> | |
<div :class="[$style.component, 'row', 'p-3']"> | |
... line 3 | |
</div> | |
</template> | |
... lines 6 - 16 |
Next, in the component itself, give it a better name: like ShoppingCartItem
:
<template> | |
<div :class="[$style.component, 'row', 'p-3']"> | |
... line 3 | |
</div> | |
</template> | |
<script> | |
export default { | |
name: 'ShoppingCartItem', | |
}; | |
</script> | |
... lines 12 - 16 |
And because this will render an individual cart item, allow that object to be passed to us as a prop. Add props
with item
, type: Object
and required: true
.
<template> | |
<div :class="[$style.component, 'row', 'p-3']"> | |
... line 3 | |
</div> | |
</template> | |
<script> | |
export default { | |
name: 'ShoppingCartItem', | |
props: { | |
item: { | |
type: Object, | |
required: true, | |
}, | |
}, | |
}; | |
</script> | |
... lines 18 - 22 |
What we're going to pass into this component will be the "complete cart item": the thing that we're looping over inside of shopping-cart
. We know that this has product
, color
and quantity
properties.
In the new template, start by printing item.product.name
.
<template> | |
<div :class="[$style.component, 'row', 'p-3']"> | |
{{ item.product.name }} | |
</div> | |
</template> | |
<script> | |
export default { | |
name: 'ShoppingCartItem', | |
props: { | |
item: { | |
type: Object, | |
required: true, | |
}, | |
}, | |
}; | |
</script> | |
... lines 18 - 22 |
Finally, at the bottom, instead of scoped styles, we're using modular styles and lang="scss"
. I'll paste in some basic styles to give each item a bottom border.
<template> | |
<div :class="[$style.component, 'row', 'p-3']"> | |
{{ item.product.name }} | |
</div> | |
</template> | |
<script> | |
export default { | |
name: 'ShoppingCartItem', | |
props: { | |
item: { | |
type: Object, | |
required: true, | |
}, | |
}, | |
}; | |
</script> | |
<style lang="scss" module> | |
@import '~styles/variables/colors.scss'; | |
.component { | |
border-bottom: 1px solid $light-component-border; | |
} | |
</style> |
Awesome! Now we could use this new component directly inside of shopping-cart.vue
: we would v-for
over this component.
And... that would be fine! But instead, I'm going to create another component that's between shopping-cart
and cart-item
: its job will be to render the entire "cart list", including the "your cart is empty message". You don't have to do this... but because the shopping-cart
component is going to get more and more complex - including eventually rendering a checkout form - I want to do this now to help keep that component "sane".
In the shopping-cart/
directory, create a new index.vue
. I'll paste in a basic template. This receives an items
array... and already has a v-if
to show the "you cart is empty" message if there are no items.
<template> | |
<div> | |
<div v-if="items.length === 0"> | |
Your cart is empty! Get to shopping! | |
</div> | |
</div> | |
</template> | |
<script> | |
export default { | |
name: 'ShoppingCartList', | |
props: { | |
items: { | |
type: Array, | |
required: true, | |
}, | |
}, | |
}; | |
</script> |
All we need to do is loop over those items and render cart-item
.
Start by importing that component: import ShoppingCartItem
from @/components/shopping-cart/cart-item
:
... lines 1 - 14 | |
<script> | |
import ShoppingCartItem from '@/components/shopping-cart/cart-item'; | |
export default { | |
name: 'ShoppingCartList', | |
... lines 20 - 28 | |
}; | |
</script> |
Then add the components
key and pop that inside:
... lines 1 - 14 | |
<script> | |
import ShoppingCartItem from '@/components/shopping-cart/cart-item'; | |
export default { | |
name: 'ShoppingCartList', | |
components: { | |
ShoppingCartItem, | |
}, | |
... lines 23 - 28 | |
}; | |
</script> |
Up in the template, we can use the v-for
directly on that component: <shopping-cart-item
then v-for=
with the long syntax: (item, index) in items
. The reason we're doing this is that each item in the cart does not have a unique key. So, temporarily, we will say :key=index
. That's not ideal, but we'll improve it soon. Finally, pass the required prop: :item="item"
<template> | |
<div> | |
... lines 3 - 6 | |
<shopping-cart-item | |
v-for="(item, index) in items" | |
:key="index" | |
:item="item" | |
/> | |
</div> | |
</template> | |
... lines 14 - 31 |
Ok! This component is ready! Let's use it in shopping-cart
. Import it: import ShoppingCartList
from @/components/shopping-cart
, add that to the components
option and... we get to delete a bunch of code!
... lines 1 - 21 | |
<script> | |
... lines 23 - 26 | |
import ShoppingCartList from '@/components/shopping-cart'; | |
... lines 28 - 29 | |
export default { | |
name: 'ShoppingCart', | |
components: { | |
... line 33 | |
ShoppingCartList, | |
... line 35 | |
}, | |
... lines 37 - 78 | |
}; | |
</script> | |
... lines 81 - 91 |
Replace this with <shopping-cart-list
. Oh, but we need to be careful: the completeCart
variable is not available immediately. So add v-if="completeCart"
and then pass in the prop: :items=completeCart.items
.
<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"> | |
... lines 10 - 11 | |
<shopping-cart-list | |
v-if="completeCart" | |
:items="completeCart.items" | |
/> | |
</div> | |
</div> | |
</div> | |
</div> | |
</template> | |
... lines 21 - 91 |
Time to try this thing! Find your browser and... we got it! It doesn't look like much yet, but we have a flexible component structure that we can use to add more features... without making any one component insanely complex.
But before we add more stuff, in index.vue
, it does bother me that we're still using the index
from the array as the key
in v-for
. This... "might" be ok... but it could cause rendering problems. Next: let's fix this and then celebrate by adding more structure and data to the cart.
"Houston: no signs of life"
Start the conversation!
// 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
}
}