Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Cart Structure

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

We 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>

Creating the cart-item Component

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>

Creating the Cart "list" Component

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.

Leave a comment!

0
Login or Register to join the conversation
Cat in space

"Houston: no signs of life"
Start the conversation!

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
    }
}
userVoice