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 SubscribeThanks 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.
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!
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!
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.
"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
}
}