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 SubscribeClick to go back to view the shopping cart... which is now empty. We're not done shopping! Add another inflatable sofa. Awesome!
Server-side validation is a must: we can't have API endpoints that allow any data. And we've handled that nicely. Go us!
Client-side validation in JavaScript is optional. It requires extra work, but it can make your form even nicer to use. So let's add some! We'll keep it simple: on "blur" of a field - so when we leave a field - if that field is empty, we'll immediately show a validation error.
Start inside of our checkout form component... down in methods
. Add a new one called validateField()
. I'll paste in the start: an object that contains the validation messages that should be used for each field if that field is blank.
... lines 1 - 71 | |
<script> | |
... lines 73 - 77 | |
export default { | |
name: 'CheckoutForm', | |
... lines 80 - 104 | |
methods: { | |
... lines 106 - 147 | |
validateField() { | |
const validationMessages = { | |
customerName: 'Please, enter your full name!', | |
customerEmail: 'Please, enter your email address!', | |
customerAddress: 'Please, enter your street address!', | |
customerZip: 'Please, enter your ZIP code!', | |
customerCity: 'Please, enter your city!', | |
customerPhone: 'Please, provide a phone number!', | |
}; | |
}, | |
}, | |
}; | |
</script> |
Before we add the real logic to this method, let's call this "on blur" of each field. Easy! Head up to the template and add @blur="validateField"
.
<template> | |
<div class="row p-3"> | |
<div class="col-12"> | |
<form @submit.prevent="onSubmit"> | |
... lines 5 - 12 | |
<div class="form-row"> | |
<form-input | |
... lines 15 - 17 | |
@blur="validateField" | |
/> | |
... lines 20 - 27 | |
</div> | |
... lines 29 - 72 | |
</form> | |
</div> | |
</div> | |
</template> | |
... lines 77 - 175 |
Except... bah! That won't work! This is a custom component, not a normal form element.
Okay, don't panic. Go into form-input.vue
. And on the actual <input>
element, add @blur=""
... and then we'll just emit that same blur
event from here... and pass it the same event data.
<template> | |
<div class="form-group"> | |
... lines 3 - 8 | |
<input | |
... lines 10 - 18 | |
@blur="$emit('blur', $event)" | |
> | |
... lines 21 - 26 | |
</div> | |
</template> | |
... lines 29 - 62 |
We're basically propagating that event up so that our parent component can listen to it.
Back in index.vue
, now that this should work, copy the @blur
and repeat this on all six fields... super fast!
<template> | |
<div class="row p-3"> | |
<div class="col-12"> | |
<form @submit.prevent="onSubmit"> | |
... lines 5 - 12 | |
<div class="form-row"> | |
<form-input | |
v-model="form.customerName" | |
... lines 16 - 17 | |
@blur="validateField" | |
/> | |
<form-input | |
v-model="form.customerEmail" | |
... lines 23 - 25 | |
@blur="validateField" | |
/> | |
</div> | |
<form-input | |
v-model="form.customerAddress" | |
... line 32 | |
@blur="validateField" | |
/> | |
<div class="form-row"> | |
<form-input | |
v-model="form.customerZip" | |
... lines 39 - 40 | |
@blur="validateField" | |
/> | |
<form-input | |
v-model="form.customerCity" | |
... lines 46 - 47 | |
@blur="validateField" | |
/> | |
<form-input | |
v-model="form.customerPhone" | |
... lines 53 - 55 | |
@blur="validateField" | |
/> | |
</div> | |
... lines 59 - 72 | |
</form> | |
</div> | |
</div> | |
</template> | |
... lines 77 - 175 |
Ok! Head back down to validateField()
... here it is. The plan is: read the id
attribute of whatever input was just blurred, use that to see if the form data for that field is empty and, if it is, add a new item to the validationErrors
data.
To read the id
attribute, we need the event
argument:
... lines 1 - 77 | |
<script> | |
... lines 79 - 83 | |
export default { | |
name: 'CheckoutForm', | |
... lines 86 - 110 | |
methods: { | |
... lines 112 - 153 | |
validateField(event) { | |
... lines 155 - 170 | |
}, | |
}, | |
}; | |
</script> |
Then, down here, we can say const validationField = event.target.id
:
... lines 1 - 77 | |
<script> | |
... lines 79 - 83 | |
export default { | |
name: 'CheckoutForm', | |
... lines 86 - 110 | |
methods: { | |
... lines 112 - 153 | |
validateField(event) { | |
... lines 155 - 163 | |
const validationField = event.target.id; | |
... lines 165 - 170 | |
}, | |
}, | |
}; | |
</script> |
so that will be something like customerName
or customerEmail
. Then, if not this.form[validationField]
, then we know that field is empty. Add a validation error with this.validationErrors[validationField] =
validationMessages[validationField]
.
... lines 1 - 77 | |
<script> | |
... lines 79 - 83 | |
export default { | |
name: 'CheckoutForm', | |
... lines 86 - 110 | |
methods: { | |
... lines 112 - 153 | |
validateField(event) { | |
... lines 155 - 163 | |
const validationField = event.target.id; | |
if (!this.form[validationField]) { | |
this.validationErrors[validationField] = validationMessages[validationField]; | |
... lines 168 - 169 | |
} | |
}, | |
}, | |
}; | |
</script> |
Oh, and don't forget an else
: in case the user just went back... filled something in... and then blurred. If there was already an error before, now we need to remove it. Do that with delete this.validationErrors[validationField]
.
... lines 1 - 77 | |
<script> | |
... lines 79 - 83 | |
export default { | |
name: 'CheckoutForm', | |
... lines 86 - 110 | |
methods: { | |
... lines 112 - 153 | |
validateField(event) { | |
... lines 155 - 163 | |
const validationField = event.target.id; | |
if (!this.form[validationField]) { | |
this.validationErrors[validationField] = validationMessages[validationField]; | |
} else { | |
delete this.validationErrors[validationField]; | |
} | |
}, | |
}, | |
}; | |
</script> |
The reason I'm using delete
and not equals null is... well... that's just the way that our validationErrors
object works right now. It starts as an empty object and we only put things onto it when they have an error. We even reset it back to an empty object on submit. This will actually cause a "reactivity" problem... which we'll explore in a few minutes.
By the way, there are some Vue libraries to help with validation, like Vuelidate. You can totally check these out. They make adding a lot of complex validation rules a lot easier and cleaner. Right now, we're just checking to see if the field is blank. But even with something like Vuelidate, you're still basically doing the same thing: you're storing the errors for a field somewhere like validationErrors
and then using that when you render to display the error.
Anyways, let's test this thing! Find your browser, click "Check Out", click into the name field... and hit tab. Huh. Nothing happened. Go check out the Vue dev tools, find CheckoutForm
... then down to validationErrors
. Yea, it says that we do have two validation errors for name
and email
: the two fields we blurred.
So the data is right.... but they're not rendering on the form. Want to see something even weirder? Go into one of the other boxes and type something. Boom! As soon as we do, both error messages show up! We don't see any errors until we modify a field.
What the heck is going on? The short answer is that we accidentally removed reactivity from the validationErrors
data. That's a fancy way of saying that when we change a value on validationErrors
, Vue does not realize that it needs to re-render. So the data is being stored correctly... but it doesn't reflect on the page until Vue re-renders for some other reason, like us changing some of the form data.
Next: let's explore this deeper... and fix it!
// 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
}
}
Looks that in Vue3 in works fine from the box, so error appears immediately on tab-click