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 SubscribeShopping cart... check! There's just one more feature we need to add to the site: the ability to fill out a checkout form. No... we won't create a real checkout form with a credit card field and payment processing. That's not the point of this tutorial. But we will build a real form for the customer's info with real validation: both server-side and client-side.
So: should this checkout form be a new page? We could do that. But this time, instead, I'm going to add that form right to this page. This one component will hold both steps of the checkout process. Step one will show this cart and then, by clicking a button, the user will go to step two: the checkout form. This will be a great opportunity to chat about Vue transitions so that this process feels really smooth.
Let's get to work. In assets/pages/shopping-cart.vue
, this component renders the entire page - including cart-sidebar
and shopping-cart-list
.
Add a div to hold the "Checkout" button. Give it a v-if=
so that we only render this once the completeCart
data has finished loading and if completeCart.items.length > 0
. We don't want to show a checkout button if the cart is empty.
<template> | |
<div :class="[$style.component, 'container-fluid']"> | |
<div class="row"> | |
... lines 4 - 18 | |
<div class="col-xs-12 col-lg-9"> | |
... lines 20 - 21 | |
<div class="content p-3"> | |
... lines 23 - 34 | |
<div | |
v-if="completeCart && completeCart.items.length > 0" | |
> | |
... lines 38 - 40 | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
</template> | |
... lines 47 - 139 |
Inside, say <button class="btn btn-primary">
and "Check Out!".
<template> | |
<div :class="[$style.component, 'container-fluid']"> | |
<div class="row"> | |
... lines 4 - 18 | |
<div class="col-xs-12 col-lg-9"> | |
... lines 20 - 21 | |
<div class="content p-3"> | |
... lines 23 - 34 | |
<div | |
v-if="completeCart && completeCart.items.length > 0" | |
> | |
<button class="btn btn-primary"> | |
Check Out! | |
</button> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
</template> | |
... lines 47 - 139 |
Let's see how that looks. Nice!
Our ShoppingCart
component is going to have two, sort of, "states" that the user can toggle between: the "cart" state that shows the shopping cart and the "checkout" state that shows the checkout form. We're going to need a piece of data to keep track of the current state.
Add a new currentState
data that defaults to cart
. I'll... make sure I spell that correctly.
... lines 1 - 50 | |
<script> | |
... lines 52 - 59 | |
export default { | |
name: 'ShoppingCart', | |
... lines 62 - 68 | |
data() { | |
return { | |
currentState: 'cart', | |
... lines 72 - 74 | |
}; | |
}, | |
... lines 77 - 133 | |
}; | |
</script> | |
... lines 136 - 146 |
Next, when the user clicks the button, let's call a new method to change that data to the other state: @click="switchState"
.
<template> | |
<div :class="[$style.component, 'container-fluid']"> | |
<div class="row"> | |
... lines 4 - 18 | |
<div class="col-xs-12 col-lg-9"> | |
... lines 20 - 21 | |
<div class="content p-3"> | |
... lines 23 - 34 | |
<div | |
v-if="completeCart && completeCart.items.length > 0" | |
> | |
<button | |
class="btn btn-primary" | |
@click="switchState" | |
> | |
Check Out! | |
</button> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
</template> | |
... lines 50 - 146 |
Copy that name and head down to the bottom of the component to add that method: switchState()
. Inside, since we only have 2 states, it's pretty simple: this.currentState =
, then if this.currentState === 'cart'
, set it to checkout
, else we are in the checkout
state and want to go back to cart
. The button will let us go back and forth.
... lines 1 - 50 | |
<script> | |
... lines 52 - 59 | |
export default { | |
name: 'ShoppingCart', | |
... lines 62 - 109 | |
methods: { | |
switchState() { | |
this.currentState = this.currentState === 'cart' ? 'checkout' : 'cart'; | |
}, | |
... lines 114 - 132 | |
}, | |
}; | |
</script> | |
... lines 136 - 146 |
We're not using currentState
in the template yet... but we can at least check to see if clicking this button changes that data. Go to Vue dev tools, find ShoppingCart
and find currentState
. Yep, it's cart
.
Click the button. Huh. It didn't change! Spoiler alert: it did change! If you click off of that component and back on... and go find that data again, it did change! This is a quirk with the Vue dev tools. If you change a piece of data, but that data doesn't cause anything to re-render. the dev tools won't instantly update. No big deal: it is working.
So let's render something based on the data... actually two things. When we click the button, the title should change from "Shopping Cart" to "Checkout" and the text on the button itself should go from "Check out!" to "Go Back".
Instead of hardcoding this logic and messages in the template, let's leverage some computed props. Find the computed
section and call the first pageTitle
. return
if this.currentState === 'cart'
then Shopping Cart
, else Checkout
:
... lines 1 - 50 | |
<script> | |
... lines 52 - 59 | |
export default { | |
name: 'ShoppingCart', | |
... lines 62 - 76 | |
computed: { | |
... lines 78 - 100 | |
pageTitle() { | |
return this.currentState === 'cart' | |
? 'Shopping Cart' | |
: 'Checkout'; | |
}, | |
... lines 106 - 110 | |
}, | |
... lines 112 - 144 | |
}; | |
</script> | |
... lines 147 - 157 |
Copy this and do something similar for buttonText
, returning Check Out
if we're on the cart page and Back
if we're on checkout:
... lines 1 - 50 | |
<script> | |
... lines 52 - 59 | |
export default { | |
name: 'ShoppingCart', | |
... lines 62 - 76 | |
computed: { | |
... lines 78 - 105 | |
buttonText() { | |
return this.currentState === 'cart' | |
? 'Check Out >>' | |
: '<< Back'; | |
}, | |
}, | |
... lines 112 - 144 | |
}; | |
</script> | |
... lines 147 - 157 |
Very nice! Scroll up to use these. Change the title to :text="pageTitle"
:
<template> | |
<div :class="[$style.component, 'container-fluid']"> | |
<div class="row"> | |
... lines 4 - 18 | |
<div class="col-xs-12 col-lg-9"> | |
<title-component :text="pageTitle" /> | |
... lines 21 - 45 | |
</div> | |
</div> | |
</div> | |
</template> | |
... lines 50 - 157 |
and... for the button, {{ buttonText }}
:
<template> | |
<div :class="[$style.component, 'container-fluid']"> | |
<div class="row"> | |
... lines 4 - 18 | |
<div class="col-xs-12 col-lg-9"> | |
... lines 20 - 21 | |
<div class="content p-3"> | |
... lines 23 - 34 | |
<div | |
v-if="completeCart && completeCart.items.length > 0" | |
> | |
<button | |
class="btn btn-primary" | |
@click="switchState" | |
> | |
{{ buttonText }} | |
</button> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
</template> | |
... lines 50 - 157 |
Let's check it! Back at the browser... click the button. Got it!
The last step is to toggle the content on the main part of the page. The shopping cart list is already isolated into its own component. Let's now create another component to hold the checkout form... so we don't have to add all its HTML and logic right into shopping-cart
.
Create a new checkout/
directory and then an index.vue
file inside. I'll paste in a very simple component that prints some text and exports a name.
<template> | |
<div class="row p-3"> | |
<div class="col-12"> | |
A cool checkout form will appear right here! | |
</div> | |
</div> | |
</template> | |
<script> | |
export default { | |
name: 'CheckoutForm', | |
}; | |
</script> |
Back in shopping-cart.vue
, use this. Step one: import it: import CheckoutForm from
@/components/checkout
... but where is my auto-complete? Ah! Ryan! I accidentally put the new checkout/
directory in assets/
. It should live in components/
. Now Webpack is happy.
Add CheckoutForm
to components and scroll up to the template.
... lines 1 - 54 | |
<script> | |
... lines 56 - 58 | |
import CheckoutForm from '@/components/checkout'; | |
... lines 60 - 64 | |
export default { | |
name: 'ShoppingCart', | |
components: { | |
CheckoutForm, | |
... lines 69 - 72 | |
}, | |
... lines 74 - 150 | |
}; | |
</script> | |
... lines 153 - 163 |
Before we render this, look at <shopping-cart-list
. Thanks to the v-if
, this only renders once the completeCart
data is available. Now we want to render it when the completeCart
data is there and if the currentState
equals cart.
<template> | |
<div :class="[$style.component, 'container-fluid']"> | |
<div class="row"> | |
... lines 4 - 18 | |
<div class="col-xs-12 col-lg-9"> | |
... lines 20 - 24 | |
<shopping-cart-list | |
v-if="completeCart && currentState === 'cart'" | |
... lines 27 - 32 | |
/> | |
... lines 34 - 49 | |
</div> | |
</div> | |
</div> | |
</template> | |
... lines 54 - 163 |
Below this, render <checkout-form
. Oh, but copy the v-if
from above, paste, and render it when the currentState
is checkout
.
<template> | |
<div :class="[$style.component, 'container-fluid']"> | |
<div class="row"> | |
... lines 4 - 18 | |
<div class="col-xs-12 col-lg-9"> | |
... lines 20 - 24 | |
<shopping-cart-list | |
v-if="completeCart && currentState === 'cart'" | |
... lines 27 - 32 | |
/> | |
... line 34 | |
<checkout-form | |
v-if="completeCart && currentState === 'checkout'" | |
/> | |
... lines 38 - 49 | |
</div> | |
</div> | |
</div> | |
</template> | |
... lines 54 - 163 |
By the way, you could make a really good argument that the currentState
logic should live in v-show
instead of v-if
. We'll talk more about that at the end of our discussion about Vue transitions.
Anyways, let's do this! Find your browser and click "Check Out". Beautiful! We can switch back and forth, back and forth.
But... the change is a little.. abrupt. I'd love to have a nice transition between the two pages, like the old one fades out and the new one fades in. Can we do that? Of course! CSS itself supports transitions and with a special feature from Vue, we can leverage those perfectly. Let's learn how next.
"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
}
}