Add to Cart Controls on the Sidebar

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

Now that we're rendering the featured product on the sidebar, our next job is to make it work! Make it possible to add the item to the cart right from this page.

Thanks to our wonderful work isolating the "add to cart" controls into its own component, this is going to be fun! Practically a victory lap!

Rendering the Cart Controls

In cart-sidebar.vue, let's get to work: import CartAddControls from @/components/product-show/cart-add-controls, add a components key, then put that inside.

... lines 1 - 29
<script>
... line 31
import CartAddControls from '@/components/product-show/cart-add-controls';
export default {
name: 'ShoppingCartSidebar',
components: {
CartAddControls,
},
... lines 39 - 49
};
</script>
... lines 52 - 65

Up in the template, after the h6 render <cart-add-controls. This needs a number of different props and if I hit Command+Spacebar PhpStorm will show them to me: 3 boolean props and a product object prop.

<template>
<div :class="[$style.component, 'p-3', 'mb-5']">
... lines 3 - 12
<div class="pt-3">
... lines 14 - 19
<cart-add-controls
... lines 21 - 24
/>
</div>
</div>
</template>
... lines 29 - 65

Pass :product set to featuredProduct. For the other three, hardcode them for now: I want to see this render first: :add-to-cart-loading="false", :add-to-cart-success="false" and :allow-add-to-cart="false".

<template>
<div :class="[$style.component, 'p-3', 'mb-5']">
... lines 3 - 12
<div class="pt-3">
... lines 14 - 19
<cart-add-controls
:product="featuredProduct"
:add-to-cart-loading="false"
:add-to-cart-success="false"
:allow-add-to-cart="false"
/>
</div>
</div>
</template>
... lines 29 - 65

Let's go check it! And... awesome! Well, not that awesome: it looks terrible! Things aren't quite... "fitting". This button really needs to be smaller.

Adding more Flexibility to the Cart Controls Component

This is a great example of the lifecycle of a reusable component. Open cart-add-controls.vue. Until now, the "Add to Cart" text on the button did not need to be dynamic. But suddenly, we do need to be able to control it so we can make it shorter only on the sidebar.

This means our component needs to be made more configurable with a new prop. Call it addButtonText, the type is String, but this time, instead of making it required, set a default value of Add to Cart.

... lines 1 - 32
<script>
... lines 34 - 35
export default {
name: 'ProductCartAddControls',
... lines 38 - 40
props: {
... lines 42 - 45
addButtonText: {
type: String,
default: 'Add to Cart',
},
... lines 50 - 61
},
... lines 63 - 79
};
</script>
... lines 82 - 90

Back up in the template, replace the hardcoded text with {{ addButtonText }}.

<template>
<div :class="[$style.component, 'd-flex', 'align-items-center', 'justify-content-center']">
... lines 3 - 14
<button
... lines 16 - 18
>
{{ addButtonText }}
... lines 21 - 28
</button>
</div>
</template>
... lines 32 - 90

Over in cart-sidebar, let's leverage this new flexibility: pass the add-button-text prop set to +. We don't need to say :add-button-text because we're not setting this to a variable or JavaScript expession: it's just a plus sign.

<template>
<div :class="[$style.component, 'p-3', 'mb-5']">
... lines 3 - 12
<div class="pt-3">
... lines 14 - 19
<cart-add-controls
... lines 21 - 24
add-button-text="+"
/>
</div>
</div>
</template>
... lines 30 - 66

Now when we refresh.. yes! So much better!

Passing in the Real Prop Values

Let's make the button actually work... and we're super close! Step one is to pass real values for the 3 hardcoded props. All of this information really lives up in our parent ShoppingCart component. There, we're using the get-shopping-cart mixin, which holds a number of pieces of data, including addToCartLoading and addToCartSuccess. And so, we're going to need to do a little bit of prop passing: we need to pass this info from shopping-cart into cart-sidebar... so that it can pass them into cart-add-controls. Passing props down several components isn't my favorite thing to do in Vue, but it's fairly simple.

Start by stealing the three props from cart-add-controls. Copy them... and paste those into cart-sidebar:

... lines 1 - 30
<script>
... lines 32 - 34
export default {
name: 'ShoppingCartSidebar',
... lines 37 - 44
allowAddToCart: {
type: Boolean,
required: true,
},
addToCartLoading: {
type: Boolean,
required: true,
},
addToCartSuccess: {
type: Boolean,
required: true,
},
},
... lines 58 - 62
};
</script>
... lines 65 - 78

Use these up in the template: addToCartLoading, addToCartSuccess and allowAddToCart:

<template>
<div :class="[$style.component, 'p-3', 'mb-5']">
... lines 3 - 12
<div class="pt-3">
... lines 14 - 19
<cart-add-controls
... line 21
:add-to-cart-loading="addToCartLoading"
:add-to-cart-success="addToCartSuccess"
:allow-add-to-cart="allowAddToCart"
... line 25
/>
</div>
</div>
</template>
... lines 30 - 78

Finally, we need to pass these props into cart-sidebar from shopping-cart. Scroll to the template. Yep! <cart-sidebar is mad because it's missing 3 required props! Set :allow-add-to-cart to cart !== null because the user should be allowed to add an item to the cart once the cart data has been loaded from AJAX. Then :add-to-cart-success set to addToCartSuccess and :add-to-cart-loading set to addToCartLoading.

<template>
<div :class="[$style.component, 'container-fluid']">
<div class="row">
<aside class="col-xs-12 col-lg-3">
<cart-sidebar
... lines 6 - 7
:allow-add-to-cart="cart !== null"
:add-to-cart-success="addToCartSuccess"
:add-to-cart-loading="addToCartLoading"
/>
</aside>
... lines 13 - 29
</div>
</div>
</div>
</template>
... lines 34 - 125

By the way, I've repeated this cart !== null logic both here and in the product-show template. If we wanted, we could add a new allowAddToCart computed property to the mixin to make this easier.

Listening to the add-to-cart Event

Anyways, the last step to make this work is to make the button do something when it's clicked. Remember: in cart-add-controls, when we click that button... it calls an addToCart method... and that emits an event: add-to-cart. We can listen to that to do the logic.

In cart-sidebar, add @add-to-cart=. And, hmm. The cart data we need to modify does not live in this component... so we need to emit the event again so shopping-cart can use it. Do it with $emit('add-to-cart', $event).

<template>
<div :class="[$style.component, 'p-3', 'mb-5']">
... lines 3 - 12
<div class="pt-3">
... lines 14 - 19
<cart-add-controls
... lines 21 - 25
@add-to-cart="$emit('add-to-cart', $event)"
/>
</div>
</div>
</template>
... lines 31 - 79

Then, in shopping-cart, listen to that. On <cart-sidebar, say @add-to-cart=. Let's think... in the get-shopping-cart mixin, we have an addProductToCart method. Ah, we can call that directly: addProductToCart() with the three arguments it needs: featuredProduct, selectedColorId, which is on the event - so $event.selectedColorId - and the quantity - also on the event: $event.quantity.

<template>
<div :class="[$style.component, 'container-fluid']">
<div class="row">
<aside class="col-xs-12 col-lg-3">
<cart-sidebar
... lines 6 - 10
@add-to-cart="addProductToCart(
featuredProduct,
$event.selectedColorId,
$event.quantity
)"
/>
</aside>
... lines 18 - 35
</div>
</div>
</template>
... lines 39 - 130

Phew! That was a good amount of work to connect all the pieces together, but I think we're done! Find your browser and do a full refresh. Let's see what happens. I already have 3 blue sofas in my cart. Let's add 2 more. Yes! This is so cool! The cart instantly updated!

Add a couple more red sofas... boom... and green isn't in the cart yet... but that works too! Even if we remove an item... and add it back: it all looks perfect.

But... yes there is a but... we have a subtle bug. If the featured product - the inflatable sofa - were not already in the cart when the page loaded, then clicking the plus button would make things go bananas. Let's find out why next and solve it with a deeper understanding of watcher functions.

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