Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Advancing Between Cart & Checkout

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

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

Adding the Checkout Button

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!

The "currentState" Data

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

Vue Dev Tools Quirk when no Re-Render

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.

Toggling the Title Between States

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!

Creating the Checkout Form Component

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.

Leave a comment!

0
Login or Register to join the conversation
Cat in space

"Houston: no signs of life"
Start the conversation!

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