Having validation on the client side is a must in any modern website. But doing it isn’t a very fun task. But with VueJS and the Vuelidate plugin, we can make this task so easy that we don’t have to think about it again.
To show you how this is done, we’ll create an example app from scratch that has a single page with a signup form.
Setting up the app
I’m using vue-cli to create a new vue project with the webpack template. So run this in the terminal:
vue init webpack form-validation
Make sure vue-router
is included as we’re going to use it in this demo.
In this step, we’re going to prepare our signup page so we can build the form in the next step.
Let’s first remove the image and fix some stylings in App.vue
.
It should look like this:
<template>
<div id="app">
<router-view />
</div>
</template>
<script>
export default {
name: 'App'
}
</script>
<style>
* {
box-sizing: border-box;
}
html,
body {
height: 100%;
margin: 0;
padding: 0;
}
#app {
font-family: 'Avenir', Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
color: #2c3e50;
height: 100%;
}
</style>
Out of the box, the vue webpack template gives us an example page component called HelloWorld
, which you can find in src/components
directory. Replace that file with a component named SignupPage
. And put this into it:
<template>
<div class="signup-page">Signup page</div>
</template>
<script>
export default {}
</script>
<style scoped></style>
Now we need to link the root url to that component. To do that, change the component name from HelloWorld
to SignupPage
inside src/router/index.js
file.
After that you should see the word “Signup Page” when viewing localhost:8080/#/
Creating the form
We’re going to include our form fields inside a separate component called SignupForm
. So create that file in src/components
.
Next, modify SignupPage
to include that component along with some markup and stylings.
<template>
<div class="signup-page">
<div class="content">
<signup-form />
</div>
</div>
</template>
<script>
import SignupForm from '@/components/SignupForm'
export default {
name: 'SignupPage',
components: { SignupForm }
}
</script>
<style scoped>
.signup-page {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
background: rgba(0, 160, 140, 0.1);
}
.content {
width: 500px;
box-shadow: 0 2px 5px 0px rgba(0, 0, 0, 0.2);
padding: 40px 10px;
background: white;
border-radius: 2px;
}
.message {
text-align: center;
}
</style>
Let’s now fill our SignupForm
component with the fields we need for our form.
<template>
<div class="signup-form">
<div class="field">
<label class="label">Username</label>
<input v-model="username" type="text" class="text-input" />
<span class="error-message"> This field is required </span>
</div>
<div class="field">
<label class="label">Email</label>
<input v-model="email" type="text" class="text-input" />
<span class="error-message"> This field is required </span>
</div>
<div class="field">
<label class="label">Password</label>
<input v-model="password" type="password" class="text-input" />
<span class="error-message"> This field is required </span>
</div>
<div class="field">
<label class="label">Confirm Password</label>
<input
v-model="passwordConfirmation"
type="password"
class="text-input"
/>
<span class="error-message"> This field is required </span>
</div>
<button class="button">Sign Up</button>
</div>
</template>
<script>
export default {
name: 'SignupForm',
data() {
return {
username: '',
email: '',
password: '',
passwordConfirmation: ''
}
}
}
</script>
<style scoped>
.signup-form {
display: flex;
flex-direction: column;
align-items: center;
}
.field {
display: flex;
flex-direction: column;
width: 400px;
}
.label {
color: #555;
font-size: 12px;
font-weight: bold;
text-transform: uppercase;
}
.text-input {
border: none;
box-shadow: 0 0 1px 0 rgba(0, 0, 0, 0.3) inset;
outline: none;
padding: 5px;
font-size: 14px;
color: #444444;
border-radius: 2px;
transition: box-shadow 0.2s;
margin-top: 3px;
}
.text-input:focus {
box-shadow: 0 0 1px 0 rgba(0, 0, 0, 0.4) inset;
}
.field + .field {
margin-top: 15px;
}
.button {
align-self: flex-end;
margin: 20px 40px 0;
border-radius: 4px;
box-shadow: none;
border: none;
background: rgba(0, 160, 140, 0.8);
color: #ffffff;
font-weight: bold;
text-transform: uppercase;
padding: 7px 20px;
font-size: 15px;
font-weight: normal;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: background 0.2s;
outline: none;
}
.button:hover {
background: rgba(0, 160, 140, 1);
}
.error-message {
color: #b22222;
font-size: 13px;
margin: 5px 0 0 5px;
}
</style>
There’s nothing to explain about this code. We just created the needed fields for our signup form, and we linked each field with the proper data property.
Implementing the submit action
To make this demo more useful, we need to see what will happen if the form submits successfully. To make this simple, we just need to display some kind of message like “Done!”.
To do that, replace what’s inside <div class="content">
element in SignupPage.vue
with this:
<signup-form v-if="!submitted" @submit="submitted = true" />
<h1 v-else class="message">Done!</h1>
To make this work, we have to define that submitted
data property.
data () {
return {
submitted: false
}
}
Also, we need to emit a submit
event when the signup button is clicked.
Go to SignupForm
component, and listen for the click event on the signup button.
<button class="button" @click="submit">Sign Up</button>
Then define that submit action in methods
.
methods: {
submit () {
this.$emit('submit')
}
}
Now clicking on the signup button should display “Done!”.
Adding validation
As I mentioned at the beginning of this article, we’re going to use the Vuelidate plugin to validate the form.
Let’s get it installed first.
npm install vuelidate --save
Then import it in main.js
.
import Vuelidate from 'vuelidate'
Vue.use(Vuelidate)
The first step in using Vuelidate is attach the validation rules to the needed data properties — which are currently tied to our form fields.
We can do that inside a new option called validations
. Add that bellow data()
inside SignupForm.vue
.
import { required } from 'vuelidate/lib/validators'
export default {
name: 'SignupForm',
data () {
return {
username: '',
email: '',
password: '',
passwordConfirmation: ''
}
},
validations: {
username: { required },
email: { required },
password: { required },
passwordConfirmation: { required }
},
...
}
Note how we have attached the required
validation rule to each field. required
is a predefined validation rule that we can import from vuelidate/lib/validators
— all builtin validators are listed in the vuelidate docs .
Now if you open your vue devtools and switch to SignupForm
component, you’ll see a new object added to computed
section called $v
. In this object we can see all the needed information about our current form validation status.
I want you to notice four things in that object:
- Each data property we attached a validator to is listed inside that object —
username
,email
, etc. - We can check if the whole form is invalid by reading
$v.$invalid
value. $v.$dirty
is used to determine when we should display validation errors. This value is set tofalse
by default. We can change it by calling$v.$touch()
.- We can check each validation rule for each field by its name. For example, we can check the
required
key insideusername
field to know if therequired
validation passes or not.
We’re going to use three pieces of information to know when and what error message to display. If we take the username field as our first example, they would be: $v.username.$invalid
, $v.username.required
, and $v.$dirty
.
$v.username.$invalid
is used to see if theusername
field is invalid, regardless of the reason.$v.username.required
is used to check if the field is invalid because of its emptiness — that’s whatrequired
is for.$v.$dirty
is optional and not tied to that field directly. But we need to use it to display the error message only when the signup button has been clicked at least once.
Let’s integrate that solution to our username
field.
First, replace:
<span class="error-message"> This field is required </span>
With this:
<span v-if="$v.$dirty && $v.username.$invalid" class="error-message">
{{ usernameErrorMessage }}
</span>
Then define usernameErrorMessage
as computed property.
computed: {
usernameErrorMessage () {
if (!this.$v.username.required) {
return 'Username is required'
}
}
}
Now if you check that in the browser, you’ll notice that validation doesn’t work as expected. And that’s because we haven’t run our validation when clicking the signup button.
We can do this by modifying the submit
method like this:
submit () {
this.$v.$touch()
if (!this.$v.$invalid) {
this.$emit('submit')
}
}
When the submit
method is called, we mark the validation object as dirty by calling this.$v.$touch()
. And then check if the form is valid before emitting the submit
event.
The validation message for username should work as expected. Now I think it’s easy to repeat the same for the other fields.
Adding more validations
At this point, all of our fields should have the required
validation added. But this isn’t enough for all fields. For example, we should validate if the email field has a valid email format. Also, we should see if the second password field has the same value as the first password field, etc.
I think it’s enough to show you how to add a couple more and then leave the others for you.
Vuelidate has a builtin validator for emails. So we just need to import it and use it inside validations
object.
import { required, email } from 'vuelidate/lib/validators'
// ...
email: {
required, email
}
Then we just need to update emailErrorMessage
to display the proper validation error message:
emailErrorMessage () {
if (!this.$v.email.required) {
return 'Email is required'
} else if (!this.$v.email.email) {
return 'Please enter a valid email'
}
}
Now, email validation should work.
Now let’s do another one for username
. We need to validate username
to have a specific format to only allow letters, numbers, and underscores.
Since Vuelidate doesn’t have a builtin validator for that, we would need to create a custom one.
Custom validators are a simple function that returns a boolean to check if the given value is valid or not. Let’s create one for username
called validChars
:
username: {
required,
validChars: (value) => {
return (/^[a-zA-Z0-9_]+$/ig).test(value)
}
}
Then you can just use it like any other validator:
usernameErrorMessage () {
if (!this.$v.username.required) {
return 'Username is required'
} else if (!this.$v.username.validChars) {
return 'Username can only contain letters, numbers, and underscores'
}
}
Conclusion
As you can see, we started to have some pattern for how to add new validators. Just define the validator for the target field and then add the error message for it. That’s it.
To get more used to the flow, try to add a couple more to that example, like sameAs
to check if the second password is the same as the first password. And maybe minLength
to password fields to require them to have a minimum length.
Note: you can get the source code of this project from GitHub.