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 checkout form will have six fields... and now, thanks to our shiny form-input
component, we can add those with very little duplication. Copy the form-input
element and paste it five times: One, two, three, four, five.
<template> | |
<div class="row p-3"> | |
<div class="col-12"> | |
<form> | |
<form-input | |
id="customerName" | |
v-model="form.customerName" | |
label="Name:" | |
:error-message="validationErrors.customerName" | |
/> | |
<form-input | |
id="customerName" | |
v-model="form.customerName" | |
label="Name:" | |
:error-message="validationErrors.customerName" | |
/> | |
<form-input | |
id="customerName" | |
v-model="form.customerName" | |
label="Name:" | |
:error-message="validationErrors.customerName" | |
/> | |
<form-input | |
id="customerName" | |
v-model="form.customerName" | |
label="Name:" | |
:error-message="validationErrors.customerName" | |
/> | |
<form-input | |
id="customerName" | |
v-model="form.customerName" | |
label="Name:" | |
:error-message="validationErrors.customerName" | |
/> | |
<form-input | |
id="customerName" | |
v-model="form.customerName" | |
label="Name:" | |
:error-message="validationErrors.customerName" | |
/> | |
</form> | |
</div> | |
</div> | |
</template> | |
... lines 50 - 74 |
Now... I'll super quickly update each component to pass in the right props. Zoom! Each input will point to a different key on our form
data and will use a different key on validationErrors
.
<template> | |
<div class="row p-3"> | |
<div class="col-12"> | |
<form> | |
<form-input | |
id="customerName" | |
v-model="form.customerName" | |
label="Name:" | |
:error-message="validationErrors.customerName" | |
/> | |
<form-input | |
id="customerEmail" | |
v-model="form.customerEmail" | |
label="Email:" | |
:error-message="validationErrors.customerEmail" | |
/> | |
<form-input | |
id="customerAddress" | |
v-model="form.customerAddress" | |
label="Address:" | |
:error-message="validationErrors.customerAddress" | |
/> | |
<form-input | |
id="customerZip" | |
v-model="form.customerZip" | |
label="Zip Code:" | |
:error-message="validationErrors.customerZip" | |
/> | |
<form-input | |
id="customerCity" | |
v-model="form.customerCity" | |
label="City:" | |
:error-message="validationErrors.customerCity" | |
/> | |
<form-input | |
id="customerPhone" | |
v-model="form.customerPhone" | |
label="Phone Number:" | |
:error-message="validationErrors.customerPhone" | |
/> | |
</form> | |
</div> | |
</div> | |
</template> | |
... lines 50 - 74 |
Let's go see how it looks! Click to check out and... nice! I mean, it's doesn't look great, but we'll improve that soon.
Before we do, that still felt like a lot of repetition. Like, on this field, we're repeating customerCity
3 different times.
Fortunately, we can clean this up with a clever use of v-bind
. At the bottom, add a methods
key and create a new method called getFieldProps()
: This will return a map of all the props needed for a specific input. To generate that, this needs id
and label
arguments.
... lines 1 - 38 | |
<script> | |
... lines 40 - 41 | |
export default { | |
name: 'CheckoutForm', | |
... lines 44 - 59 | |
methods: { | |
... lines 61 - 67 | |
getFieldProps(id, label) { | |
... lines 69 - 73 | |
}, | |
}, | |
}; | |
</script> |
Inside, return an object with the props the field needs... which as a reminder, are id
, label
and error-message
. So, set id: id
... or better, shorten to just id
, label
and error-message
. But, this needs to be errorMessage
in camel-case: Vue will handle normalizing that to error-message
. Anyways, errorMessage
set to this.validationErrors[id]
.
... lines 1 - 38 | |
<script> | |
... lines 40 - 41 | |
export default { | |
name: 'CheckoutForm', | |
... lines 44 - 59 | |
methods: { | |
... lines 61 - 67 | |
getFieldProps(id, label) { | |
return { | |
id, | |
label, | |
errorMessage: this.validationErrors[id], | |
}; | |
}, | |
}, | |
}; | |
</script> |
Up in the template, we can use this to shorten things. Remember, :error-message
is short for v-bind:error-message
which binds a single prop. But we can also use v-bind
to bind a bunch of props at once. Remove error-message
, label
and id
and instead say v-bind
- with no colon - then getFieldProps()
passing the id and label.
<template> | |
<div class="row p-3"> | |
<div class="col-12"> | |
<form> | |
<form-input | |
v-model="form.customerName" | |
v-bind="getFieldProps('customerName', 'Name:')" | |
/> | |
<form-input | |
v-model="form.customerEmail" | |
v-bind="getFieldProps('customerEmail', 'Email:')" | |
/> | |
<form-input | |
v-model="form.customerAddress" | |
v-bind="getFieldProps('customerAddress', 'Address:')" | |
/> | |
<form-input | |
v-model="form.customerZip" | |
v-bind="getFieldProps('customerZip', 'Zip Code:')" | |
/> | |
<form-input | |
v-model="form.customerCity" | |
v-bind="getFieldProps('customerCity', 'City:')" | |
/> | |
<form-input | |
v-model="form.customerPhone" | |
v-bind="getFieldProps('customerPhone', 'Phone Number:')" | |
/> | |
</form> | |
</div> | |
</div> | |
</template> | |
... lines 38 - 78 |
There is still some repetition between v-model
and v-bind
, but it's an improvement. I'll type as fast as Fabien normally types to quickly repeat this for the other 5 fields.
Phew! Unless I messed something up (very possible), that should... not break anything. Move over, hit checkout and... awesome! Everything seems to be working!
Now let's make this look a bit nicer by organizing the fields into a few columns. This doesn't have much to do with Vue... I just don't like ugly forms.
Above the first field, add <div class="form-row">
, wrap the first two fields inside and indent them. Both elements now need an extra class. Pass class="col"
two times.
<template> | |
<div class="row p-3"> | |
<div class="col-12"> | |
<form> | |
<div class="form-row"> | |
<form-input | |
v-model="form.customerName" | |
class="col" | |
v-bind="getFieldProps('customerName', 'Name:')" | |
/> | |
<form-input | |
v-model="form.customerEmail" | |
class="col" | |
v-bind="getFieldProps('customerEmail', 'Email:')" | |
/> | |
</div> | |
... lines 18 - 42 | |
</form> | |
</div> | |
</div> | |
</template> | |
... lines 47 - 87 |
But I want to point something out. We do not have a class prop inside our <form-input>
custom component. And so, when we pass class
to this, Vue will automatically add this as an attribute to the top level element of form-input
. We can see this in the browser. If we inspect the element, yep! Both outer elements - which have a form-group
- now also have a col
class. That's exactly what we want.
Back in index.vue
, leave the customerAddress
on its own row, but wrap the last 3 fields inside of another <div class="form-row">
. Add the ending div, indent, and give all 3 of these class="col"
. And... I think I have some extra whitespace my editor is mad about. Much better.
<template> | |
<div class="row p-3"> | |
<div class="col-12"> | |
<form> | |
... lines 5 - 23 | |
<div class="form-row"> | |
<form-input | |
v-model="form.customerZip" | |
class="col" | |
v-bind="getFieldProps('customerZip', 'Zip Code:')" | |
/> | |
<form-input | |
v-model="form.customerCity" | |
class="col" | |
v-bind="getFieldProps('customerCity', 'City:')" | |
/> | |
<form-input | |
v-model="form.customerPhone" | |
class="col" | |
v-bind="getFieldProps('customerPhone', 'Phone Number:')" | |
/> | |
</div> | |
</form> | |
</div> | |
</div> | |
</template> | |
... lines 47 - 87 |
Go check it out. That looks great!
Let's make one last improvement. All of these fields are <input type="text">
. If we wanted to handle other field types like select
elements or checkboxes, we would need to do more work in <form-input>
to make it more flexible or even create some new components.
I'm not going to do that now, but I at least want to be able to render different input types, like <input type="email">
and <input type="tel">
for the phone number.
No problem!. Our <form-input>
now needs to be more flexible. So let's add a new prop. Copy the value
prop, call this one type
... and change the default to text
so that we don't have to pass this in.
... lines 1 - 28 | |
<script> | |
export default { | |
name: 'FormInput', | |
... lines 32 - 44 | |
type: { | |
type: String, | |
default: 'text', | |
}, | |
... lines 49 - 52 | |
}, | |
... lines 54 - 58 | |
}; | |
</script> |
Use this up in the template: replace type="text"
with :type="type"
.
<template> | |
<div class="form-group"> | |
... lines 3 - 8 | |
<input | |
... lines 10 - 11 | |
:type="type" | |
... lines 13 - 18 | |
> | |
... lines 20 - 25 | |
</div> | |
</template> | |
... lines 28 - 61 |
Thanks to the default value, we only need to pass this for two fields. Find customerEmail
. What's cool is that we can mix the v-bind
that's set to an entire object with other, specific props without any issues. What I mean is, when we pass in the type prop with type="email"
, that will merge nicely with whatever props getFieldProps()
adds.
<template> | |
<div class="row p-3"> | |
<div class="col-12"> | |
<form> | |
<div class="form-row"> | |
... lines 6 - 11 | |
<form-input | |
... lines 13 - 14 | |
type="email" | |
... line 16 | |
/> | |
</div> | |
... lines 19 - 44 | |
</form> | |
</div> | |
</div> | |
</template> | |
... lines 49 - 89 |
Repeat this on customerPhone
: type="tel"
.
<template> | |
<div class="row p-3"> | |
<div class="col-12"> | |
<form> | |
... lines 5 - 24 | |
<div class="form-row"> | |
... lines 26 - 37 | |
<form-input | |
... lines 39 - 40 | |
type="tel" | |
... line 42 | |
/> | |
</div> | |
</form> | |
</div> | |
</div> | |
</template> | |
... lines 49 - 89 |
Go check it! You probably won't notice any difference on a computer, but if you inspect the element... yep! It's <input type="email">
.
Okay! We are ready to set up this form to submit via Ajax. When we do that, we're going to make handling and rendering form validation a main concern.
"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
}
}