Checkout Form
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.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeThe goal of this page is to hold a checkout form. When the user submits that form, we will send an AJAX request to our API to create a "purchase" in the database, which will contain the customer's info from the form plus the list of the items in the user's cart.
To truly live our low-budget, minimum viable product hip startup mentality, instead of having a credit card form, after success, we'll redirect the user to a page with information about where to mail a check to. Very cutting edge.
Setting up the Data
Open assets/components/checkout/index.vue
:
<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> |
This component will hold a bunch of form fields like customer name, email address, etc. And it's going to need to store those field values as data. Add a data
function and return an object with a form
key set to another object. Add a key for every field: customerName
initialized to an empty string, customerEmail
, customerAddress
, customerZip
, customerCity
and customerPhone
.
// ... lines 1 - 8 | |
<script> | |
export default { | |
name: 'CheckoutForm', | |
data() { | |
return { | |
form: { | |
customerName: '', | |
customerEmail: '', | |
customerAddress: '', | |
customerZip: '', | |
customerCity: '', | |
customerPhone: '', | |
}, | |
}; | |
}, | |
}; | |
</script> |
Next, because we are definitely going to add form validation, add a validationErrors
data set to an empty object:
// ... lines 1 - 8 | |
<script> | |
export default { | |
name: 'CheckoutForm', | |
data() { | |
return { | |
form: { | |
// ... lines 15 - 20 | |
}, | |
validationErrors: {}, | |
}; | |
}, | |
}; | |
</script> |
This will eventually hold a map of which fields are currently invalid with their error message.
Adding the First Field
Nice! Up in the template, add the first field. Replace the text with a form
tag, no action
needed, then some Bootstrap markup: a div, the label - with "Name:" and a for
attribute set to customerName
. It's not necessary, but I've made this consistent with the data key... so I can keep my sanity. Add class="col-form-label"
:
<template> | |
<div class="row p-3"> | |
<div class="col-12"> | |
<form> | |
<div class="form-group"> | |
<label | |
for="customerName" | |
class="col-form-label" | |
> | |
Name: | |
</label> | |
// ... lines 12 - 17 | |
</div> | |
</form> | |
</div> | |
</div> | |
</template> | |
// ... lines 23 - 42 |
then add the <input
and make sure its id
matches the for
label attribute. Also add v-model="form.customerName"
.
<template> | |
<div class="row p-3"> | |
<div class="col-12"> | |
<form> | |
<div class="form-group"> | |
<label | |
for="customerName" | |
class="col-form-label" | |
> | |
Name: | |
</label> | |
<input | |
id="customerName" | |
v-model="form.customerName" | |
// ... lines 15 - 16 | |
> | |
</div> | |
</form> | |
</div> | |
</div> | |
</template> | |
// ... lines 23 - 42 |
Thanks to this, whenever the user updates this input, v-model
will also make sure that the form.customerName
data is also updated. Finish with type="text"
and class="form-control"
.
<template> | |
<div class="row p-3"> | |
<div class="col-12"> | |
<form> | |
<div class="form-group"> | |
<label | |
for="customerName" | |
class="col-form-label" | |
> | |
Name: | |
</label> | |
<input | |
id="customerName" | |
v-model="form.customerName" | |
type="text" | |
class="form-control" | |
> | |
</div> | |
</form> | |
</div> | |
</div> | |
</template> | |
// ... lines 23 - 42 |
Good start! And if we check it in the browser... it's there! Inspect element, open the Vue dev tools and find the CheckoutForm
component. We can watch the form.customerName
data stay in sync thanks to v-model
.
Oh, but to be extra cool, we can use v-model.trim
, which is a shortcut to trim off any extra whitespace:
<template> | |
<div class="row p-3"> | |
<div class="col-12"> | |
<form> | |
<div class="form-group"> | |
// ... lines 6 - 11 | |
<input | |
id="customerName" | |
v-model.trim="form.customerName" | |
// ... lines 15 - 16 | |
> | |
</div> | |
</form> | |
</div> | |
</div> | |
</template> | |
// ... lines 23 - 42 |
Now if I go back... and put a bunch of spaces at the start, the data does not have those.
Rendering Validation Errors
We don't have any form validation logic yet, but let's prepare our field to be able to render the validation message based on the validationErrors
data. To help, add a new methods
section at the bottom of the component with one function: isFieldValid()
with a fieldName
argument. Inside, return not fieldName in this.validationErrors
.
// ... lines 1 - 32 | |
<script> | |
export default { | |
name: 'CheckoutForm', | |
// ... lines 36 - 48 | |
methods: { | |
isFieldValid(fieldName) { | |
return !(fieldName in this.validationErrors); | |
}, | |
}, | |
}; | |
</script> |
Use this above. First, on the input, if validation fails, this needs an extra class. Change to use :class
and set form-control
to true
so that it always renders. We also want an is-invalid
class if not isFieldValid('customerName')
.
<template> | |
<div class="row p-3"> | |
<div class="col-12"> | |
<form> | |
<div class="form-group"> | |
// ... lines 6 - 11 | |
<input | |
id="customerName" | |
// ... lines 14 - 15 | |
:class="{ | |
'is-invalid': !isFieldValid('customerName'), | |
'form-control': true, | |
}" | |
> | |
// ... lines 21 - 26 | |
</div> | |
</form> | |
</div> | |
</div> | |
</template> | |
// ... lines 32 - 56 |
Then, after the input, add a span
with v-show
set to that same thing: not isFieldValid('customerName')
so that this only shows when the field is invalid. Give this class="invalid-feedback"
and print the validation error inside: validationErrors.customerName
.
<template> | |
<div class="row p-3"> | |
<div class="col-12"> | |
<form> | |
<div class="form-group"> | |
// ... lines 6 - 20 | |
<span | |
v-show="!isFieldValid('customerName')" | |
class="invalid-feedback" | |
> | |
{{ validationErrors.customerName }} | |
</span> | |
</div> | |
</form> | |
</div> | |
</div> | |
</template> | |
// ... lines 32 - 56 |
And... hmm... ah! ESLint is mad because I messed up my indentation. Good work ESLint!
Vue Router?
Let's try this! Back at the browser, the page already refreshed... which highlights one annoying thing about our setup: when we refresh, it takes us back to the cart page! This is because we are not changing the URL when we go back and forth between the cart and checkout. And so, our component can't remember that we're on a different page.
Depending on your situation, this might be ok... or it might totally not be ok! This is something that could be improved by using the Vue Router: a topic we'll talk about in a future tutorial.
Anyways, click back to the checkout form, find the Vue dev tools, locate the CheckoutForm
component, edit the validationErrors
data and set it to an object with a customerName
key and some message. Hit save. That looks lovely!
So... our customerName
field is ready! There's just one problem: do we really want to copy and paste all of this 5 more times for the other 5 fields? Um... no. We can do better. Next, let's create a new form input component that we can re-use for every field.