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 SubscribeWhen we add something to the cart, the last piece of data that's hardcoded is the color. Now, some products come in multiple colors and others don't. But when we are on a product with multiple colors - like our inflatable sofa - we need to make sure that we send the selected color's IRI with the AJAX call.
Doing this is... pretty similar to how we handled the quantity. In that case, we had an input
. When the input changes, we update this quantity
data via v-model
:
<template> | |
<div> | |
... lines 3 - 8 | |
<div | |
... lines 10 - 12 | |
> | |
... lines 14 - 30 | |
<div class="col-8 p-3"> | |
... lines 32 - 33 | |
<div class="row mt-4 align-items-center"> | |
... lines 35 - 38 | |
<div class="col-8 p-3"> | |
... lines 40 - 44 | |
<input | |
v-model.number="quantity" | |
... lines 47 - 49 | |
> | |
... lines 51 - 66 | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
</template> | |
... lines 74 - 158 |
Then, down here, it was very easy to reference this.quantity
when adding the item to the cart:
... lines 1 - 74 | |
<script> | |
... lines 76 - 82 | |
export default { | |
name: 'ProductShow', | |
... lines 85 - 95 | |
data() { | |
return { | |
... line 98 | |
quantity: 1, | |
... lines 100 - 103 | |
}; | |
}, | |
... lines 106 - 125 | |
methods: { | |
async addToCart() { | |
... lines 128 - 129 | |
await addItemToCart(this.cart, { | |
... lines 131 - 132 | |
quantity: this.quantity, | |
}); | |
... lines 135 - 136 | |
}, | |
}, | |
}; | |
</script> | |
... lines 141 - 158 |
For the color, we already have this cool color selector component. It lives in assets/components/color-selector.vue
. The important thing to know for us is that whenever we select a color, this component emits a color-selected
event and sends the IRI of that color as the event data.
... lines 1 - 13 | |
<script> | |
... lines 15 - 16 | |
export default { | |
name: 'ColorSelector', | |
... lines 19 - 27 | |
methods: { | |
selectColor(iri) { | |
this.selectedIRI = iri; | |
this.$emit('color-selected', iri); | |
}, | |
}, | |
}; | |
</script> | |
... lines 36 - 56 |
So... we can use that! In product-show.vue
, scroll up to the template and find the color selector. There it is. We've already made this only render if the product does come in multiple colors. If a product doesn't have multiple colors, the colors
array is empty. To listen to the event, add @color-selected
set to a new method: how about updateSelectedColor
.
<template> | |
<div> | |
... lines 3 - 8 | |
<div | |
... lines 10 - 12 | |
> | |
... lines 14 - 30 | |
<div class="col-8 p-3"> | |
... lines 32 - 33 | |
<div class="row mt-4 align-items-center"> | |
... lines 35 - 38 | |
<div class="col-8 p-3"> | |
<div class="d-flex align-items-center justify-content-center"> | |
<color-selector | |
v-if="product.colors.length !== 0" | |
@color-selected="updateSelectedColor" | |
/> | |
... lines 45 - 67 | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
</template> | |
... lines 75 - 169 |
Next, copy the method name and scroll down to add a new piece of data: selectedColorId
set to null:
... lines 1 - 75 | |
<script> | |
... lines 77 - 83 | |
export default { | |
name: 'ProductShow', | |
... lines 86 - 96 | |
data() { | |
return { | |
... lines 99 - 100 | |
selectedColorId: null, | |
... lines 102 - 105 | |
}; | |
}, | |
... lines 108 - 149 | |
}; | |
</script> | |
... lines 152 - 169 |
Under methods
paste updateSelectedColor()
. And because the event we're listening to sends the iri
as its data, this will receive an iri
argument:
... lines 1 - 75 | |
<script> | |
... lines 77 - 83 | |
export default { | |
name: 'ProductShow', | |
... lines 86 - 96 | |
data() { | |
return { | |
... lines 99 - 100 | |
selectedColorId: null, | |
... lines 102 - 105 | |
}; | |
}, | |
... lines 108 - 127 | |
methods: { | |
... lines 129 - 145 | |
updateSelectedColor(iri) { | |
... line 147 | |
}, | |
}, | |
}; | |
</script> | |
... lines 152 - 169 |
Inside... this.selectedColorId = iri
!
... lines 1 - 75 | |
<script> | |
... lines 77 - 83 | |
export default { | |
name: 'ProductShow', | |
... lines 86 - 96 | |
data() { | |
return { | |
... lines 99 - 100 | |
selectedColorId: null, | |
... lines 102 - 105 | |
}; | |
}, | |
... lines 108 - 127 | |
methods: { | |
... lines 129 - 145 | |
updateSelectedColor(iri) { | |
this.selectedColorId = iri; | |
}, | |
}, | |
}; | |
</script> | |
... lines 152 - 169 |
By the way, later in the tutorial, we'll learn how we could have written the color-selector
component in a way that would allow us to use v-model
on it, instead of listening to an event and creating this method. Yep, v-model
isn't just for real form inputs: it can also be used for custom components.
Anyways, up in addToCart()
, change to use color: this.selectedColorId
. Because this defaults to null
, if a product doesn't come in multiple colors, this will still be null, and everyone will be happy.
... lines 1 - 83 | |
export default { | |
name: 'ProductShow', | |
... lines 86 - 127 | |
methods: { | |
async addToCart() { | |
... lines 130 - 136 | |
await addItemToCart(this.cart, { | |
product: this.product['@id'], | |
color: this.selectedColorId, | |
quantity: this.quantity, | |
}); | |
... lines 142 - 143 | |
}, | |
... lines 145 - 148 | |
}, | |
}; | |
</script> | |
... lines 152 - 169 |
Oh, except we need to make sure that if the product does have a color, that the user selects a color before adding the item. We can do that right here: if this.product.colors.length
- so if this product comes in multiple colors - and this.selectedColorId === null
, we have a problem!
... lines 1 - 83 | |
export default { | |
name: 'ProductShow', | |
... lines 86 - 127 | |
methods: { | |
async addToCart() { | |
if (this.product.colors.length && this.selectedColorId === null) { | |
... lines 131 - 132 | |
} | |
... lines 134 - 143 | |
}, | |
... lines 145 - 148 | |
}, | |
}; | |
</script> | |
... lines 152 - 169 |
For now, I'm just going to alert('Please select a color first')
... and return.
... lines 1 - 83 | |
export default { | |
name: 'ProductShow', | |
... lines 86 - 127 | |
methods: { | |
async addToCart() { | |
if (this.product.colors.length && this.selectedColorId === null) { | |
alert('Please select a color first!'); | |
return; | |
} | |
... lines 134 - 143 | |
}, | |
... lines 145 - 148 | |
}, | |
}; | |
</script> | |
... lines 152 - 169 |
That's not a great user experience, but it's good enough for us. Solving this correctly wouldn't be much more work: I'd create a new piece of data - like addToCartError
- set that here, and render it above.
Ok: let's try this thing. Move over and... I'll refresh just to be safe. Click "Add to Cart". Alert!
Ok, we have 15 items in the cart now. Select green, quantity 1, "Add to Cart" and... it looks like it worked! Let's refresh. Yep! The cart is up to 16. In the Vue dev tools, find the cart
. Two items. And... this has quantity
1 with a color set.
At this point. I'm pretty happy with our "add to cart" feature. Well, happy except for one detail: I don't like that the shopping cart count in the header doesn't update until I refresh the page.
But... what can we do? That isn't inside of our Vue app!
The answer is... who cares? What I mean is, if it's important for us to update this count for a good user experience, we can totally do it using good, boring JavaScript.
Open the template that holds this: templates/base.html.twig
. I'll search for "shopping cart". Here it is: line 44. To make it easy to find this span
in JavaScript, add an id="js-shopping-cart-items"
.
<html lang="en-US"> | |
... lines 3 - 9 | |
<body> | |
<header class="header"> | |
<nav class="navbar navbar-expand-lg navbar-dark justify-content-between"> | |
... lines 13 - 22 | |
<ul class="navbar-nav"> | |
... lines 24 - 41 | |
<li class="nav-item"> | |
<a class="nav-link" href="#"> | |
Shopping Cart (<span id="js-shopping-cart-items">{{ count_cart_items() }}</span>) | |
</a> | |
</li> | |
</ul> | |
</nav> | |
</header> | |
... lines 50 - 69 | |
</body> | |
</html> |
Next, back in the component, after we successfully add the item to the cart, we can say document.getElementById()
- paste js-shopping-cart-items
- then .innerHTML = getCartTotalItems()
.
... lines 1 - 75 | |
<script> | |
import { fetchCart, addItemToCart, getCartTotalItems } from '@/services/cart-service.js'; | |
... lines 78 - 83 | |
export default { | |
name: 'ProductShow', | |
... lines 86 - 127 | |
methods: { | |
async addToCart() { | |
... lines 130 - 144 | |
document.getElementById('js-shopping-cart-items') | |
.innerHTML = getCartTotalItems(this.cart).toString(); | |
}, | |
... lines 148 - 151 | |
}, | |
}; | |
</script> | |
... lines 155 - 172 |
That's a new function that we haven't used yet. When I hit tab to auto-complete it, PhpStorm added the new import for us... though I'm going to move this all back onto one line.
Anyways, the function comes from cart-service
- the file we copied into our project a few minutes ago. Here it is. Very simply, it loops through the items and counts all of them using their quantity. A simple helper to get the number.
... lines 1 - 139 | |
/** | |
* Gets the total number of items in our shopping cart | |
* | |
* @param {CartCollection} cart | |
* @return {number} | |
*/ | |
export function getCartTotalItems(cart) { | |
return cart.items.reduce((acc, item) => (acc + item.quantity), 0); | |
} |
Back in product-show.vue
, down in the method, we can call getCartTotalItems()
and pass it this.cart
. Because this returns a Number, call .toString()
on the result.
... lines 1 - 75 | |
<script> | |
... lines 77 - 83 | |
export default { | |
name: 'ProductShow', | |
... lines 86 - 127 | |
methods: { | |
async addToCart() { | |
... lines 130 - 144 | |
document.getElementById('js-shopping-cart-items') | |
.innerHTML = getCartTotalItems(this.cart).toString(); | |
}, | |
... lines 148 - 151 | |
}, | |
}; | |
</script> | |
... lines 155 - 172 |
So... no. This is not the most hipster code that you will ever write. But it will work and give us the great user experience we want. If I needed to update this header from multiple places in my code, I would definitely isolate it into its own JavaScript module to avoid duplication.
Let's see if it works! Back at the browser, make sure to refresh so the span
gets the new id. Let's add 3 more green sofas. Watch the header... boom! That is so nice.
Next: with the add to cart done, let's create a new page: one that will display the shopping cart and checkout!
"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
}
}