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