Taha

How to Validate Forms in Vue.js

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 to false 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 inside username field to know if the required 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 the username 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 what required 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.

Taha Shashtari

I'm Taha Shashtari, a full-stack web developer. Building for the web is my passion. Teaching people how to do that is what I like the most. I like to explore new techniques and tools to help me and others write better code.

Subscribe to get latest updates about my work
©2024 Taha Shashtari. All rights reserved.