Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

The Cart Page

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

Thanks to the mixin, our shopping-cart component has the cart data, which is being loaded via AJAX on created. Let's use that to build this page!

import { addItemToCart, fetchCart, getCartTotalItems } from '@/services/cart-service';
export default {
data() {
return {
cart: null,
addToCartLoading: false,
addToCartSuccess: false,
};
},
async created() {
this.cart = await fetchCart();
},
methods: {
async addProductToCart(product, selectedColorId, quantity) {
if (product.colors.length && selectedColorId === null) {
alert('Please select a color first!');
return;
}
this.addToCartLoading = true;
this.addToCartSuccess = false;
await addItemToCart(this.cart, {
product: product['@id'],
color: selectedColorId,
quantity,
});
this.addToCartLoading = false;
this.addToCartSuccess = true;
document.getElementById('js-shopping-cart-items')
.innerHTML = getCartTotalItems(this.cart).toString();
},
},
};

Start with a v-if so that we don't try to use the cart data before it's loaded. <div v-if= then cart !== null.

<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">
<div v-if="cart !== null">
... lines 11 - 16
</div>
</div>
</div>
</div>
</div>
</template>
... lines 23 - 46

To know what we can do inside of this, go check out the Vue dev tools. The ShoppingCart component's cart data has an items key that we can loop over. Now, each item does not have any unique "id" property, which is a bit of an odd setup in our API, but it's fine. A unique id would really be the combination of the product IRI and the color IRI.

Anyways, let's loop over this: add div, put it on multiple lines, with v-for. I'm going to use the longer syntax so we can access the index: cartItem, index in cart.items.

I'm doing this because the cartItem doesn't have a unique id. So for right now, set :key to index. We'll improve that later with a true unique key, but it will work fine to start.

<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">
<div v-if="cart !== null">
<div
v-for="(cartItem, index) in cart.items"
:key="index"
>
... line 15
</div>
</div>
</div>
</div>
</div>
</div>
</template>
... lines 23 - 46

Inside the div, let's print cartItem.product and also cartItem.quantity.

<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">
<div v-if="cart !== null">
<div
v-for="(cartItem, index) in cart.items"
:key="index"
>
{{ cartItem.product }} ({{ cartItem.quantity }})
</div>
</div>
</div>
</div>
</div>
</div>
</template>
... lines 23 - 46

Let's see how this looks! Move over to your browser. Hey! It works! It's not very pretty or useful yet... but it's a start! Crawl before you walk, as I like to say.

When the Cart is Empty

Before I forget, let's also handle the situation where the cart is empty... we want to give those customers some encouragement to keep shopping. Down below, but still inside the v-if for cart, add <div v-if="cart.items.length === 0", then we know that

Your cart is empty. Get to shopping!

<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">
<div v-if="cart !== null">
... lines 11 - 17
<div v-if="cart.items.length === 0">
Your cart is empty! Get to shopping!
</div>
</div>
</div>
</div>
</div>
</div>
</template>
... lines 27 - 50

We can test this by messing with the Vue dev tools. Find the cart data, edit the items key and set it to an empty array. There we go!

Loading Animation

As a final touch, since the cart doesn't render until the cart AJAX call has finished, let's add a loading animation. Down on the component, start by importing our re-usable loading component: import Loading from '@/components/loading. Then add that to the components option... and in the template, use it with <loading v-show="cart === null" />.

<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="cart === null" />
... lines 11 - 22
</div>
</div>
</div>
</div>
</template>
<script>
... lines 30 - 31
import Loading from '@/components/loading';
export default {
name: 'ShoppingCart',
components: {
Loading,
TitleComponent,
},
mixins: [ShoppingCartMixin],
};
</script>
... lines 43 - 53

We could also use v-if since the loading animation will be shown once and then hidden forever... but as we talked about, these are micro-optimizations. It doesn't really matter.

Oh, and later we might need a smarter loading mechanism that uses some loading data. But right now, checking the cart is perfect: if it's set, we know that we're ready to render!

Let's try it! Move over, refresh and... yep! It was quick - but we have a loading animation!

We need More Data!

What's next? Well, we really need to render more data, like the product name and price. The problem is... we don't have that data! Look at the Vue dev tools and find the cart data. Each cart item has just these three keys. In a more perfect world, the cart API call might return more, like the actual product data instead of just the IRI string.

But since it doesn't, we're going to need to make another AJAX call to get the product data for each item in the cart.

Start by opening assets/services/products-service.js. At the bottom, I'm going to paste in a new function: fetchProductsById(), which you can copy from the code block on this page.

... lines 1 - 38
/**
* Retrieves a set of products identified by an array of IRIs
*
* @param {string[]} ids
* @return {Promise}
*/
export function fetchProductsById(ids) {
if (!ids.length) {
return Promise.resolve({ data: { 'hydra:member': [] } });
}
return axios.get(
'/api/products',
{
params: { id: ids },
},
);
}

This is going to be really useful. It will allow us to collect the product IRI string for each item in the cart, then call this function to make one AJAX call to fetch all that product data at once.

But... how exactly can we do that? In shopping-cart, we need to call this new function after the cart AJAX call has finished... because we need that data to know which product IRIs to fetch. But the cart AJAX call doesn't live in this component: it lives over in created in the mixin. How can we run some code after this finishes?

Next, let's talk about how to do this. Then we'll get to work fetching and filling in all of our missing data thanks to a clever computed property.

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