External DOM Updates

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

When 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

Listening to the color-selected Event

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

Very Basic Color Validation

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.

Updating the Shopping Cart Header Count

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".

<!DOCTYPE html>
<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!

Leave a comment!

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