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