How to validate forms in Vue.js using vuelidate

How to validate forms in Vue.js using vuelidate

Introduction

Form validation ensures that a user enters the correct data and meets the criteria defined based on the expected format before submitting the data. Examples of form validation include ensuring that email is in the correct format i.e. username followed by @ symbol and a domain name, ensuring that required input fields are not left blank, ensuring that passwords meet certain requirements e.g. password must not be less than eight (8) characters.

Vuelidate is a simple, lightweight model-based validation package for Vue.js. Vuelidate makes form validation straightforward because the validation rules are similar to the data model structure used by the form.

In this tutorial, you will learn how to use vuelidate to validate forms in a vue.js application.

Prerequisites

  • Good Understanding of HTML, CSS, and JavaScript
  • Basic Understanding of Vue.js

Setting up a Vue application

To learn about vuelidate, you will set up a vue.js application. This application is what would contain the form to be validated using vuelidate.

  npm create vue@latest

The command above installs and executes create-vue, the official vue project scaffolding tool. The command would trigger a prompt with Vue.js optional features that you may need for your project.

The optional features would be presented as such in your command line.

  ✔ Add TypeScript? … No / Yes
  ✔ Add JSX Support? … No / Yes
  ✔ Add Vue Router for Single Page Application development? … No / Yes
  ✔ Add Pinia for state management? … No / Yes
  ✔ Add Vitest for Unit testing? … No / Yes
  ✔ Add an End-to-End Testing Solution? … No / Cypress / Nightwatch / Playwright
  ✔ Add ESLint for code quality? … No / Yes
  ✔ Add Prettier for code formatting? … No / Yes
  ✔ Add Vue DevTools 7 extension for debugging? (experimental) … No / Yes

Choose whatever option that suits your use case best.

Install all the dependencies that come with a vue application

npm install

The next step is to clear out the default codes that come with a fresh vue application.

Creating a form

Open src/views/HomeView.vue and create a form


<script setup>
  import {reactive, computed} from "vue";

  const registrationData = reactive({
    username: "",
    email: "",
    tel: "",
    age: "",
    password: "",
    confirm_password: "",
  });

  function submit() {
    alert("Form submitted")
  }
</script>

Here we have a registrationData object that is a reactive data model for the two-way data binding in the form input fields. Then a submit function that would handle the form submission.


<template>
  <main>
    <form @submit.prevent="submit">
      <div>
        <label for="username">Username</label>
        <input
            type="text"
            id="username"
            placeholder="Enter your username"
            v-model="registrationData.username"
        />
      </div>
      <div>
        <label for="email">Email</label>
        <input
            type="email"
            id="email"
            placeholder="Enter your email"
            v-model="registrationData.email"
        />
      </div>
      <div>
        <label for="tel">Tel</label>
        <input
            type="tel"
            id="tel"
            placeholder="Enter your phone number"
            v-model="registrationData.tel"
        />
      </div>
      <div>
        <label for="age">Age</label>
        <input
            type="number"
            id="age"
            placeholder="Enter your age"
            v-model="registrationData.age"
        />
      </div>
      <div>
        <label for="password">Password</label>
        <input
            type="password"
            id="password"
            placeholder="Enter your password"
            v-model="registrationData.password"
        />
      </div>
      <div>
        <label for="confirm_password">Confirm password</label>
        <input
            type="password"
            id="confirm_password"
            placeholder="Confirm your password"
            v-model="registrationData.confirm_password"
        />
      </div>
      <button>Submit</button>
    </form>
  </main>
</template>

This is the registration form markup with common input fields that appear on registration forms.

main {
    display: flex;
    align-items: center;
    justify-content: center;
    flex-direction: column;
    min-height: 100vh;
    background-color: #d1d5db;
}

form {
    width: 500px;
    background: white;
    padding: 16px;
}

form > div {
    display: flex;
    flex-direction: column;
    gap: 6px;
    margin-bottom: 10px;
}

form > div > input {
    border: 1px solid #9ca3af;
    padding: 4px 6px;
}

form > div > label {
    font-size: 14px;
    font-weight: 600;
    color: #252525;
}

form > div > span {
    font-size: 12px;
    color: #dc2626;
}

input:focus {
    border: none;
    outline: 1px solid #000;
}

button {
    width: 100%;
    border: none;
    background-color: #6941c6;
    color: white;
    padding: 10px 0;
}

At this point the registration form is all set up. Completed form This is what the form should look like at this point.

Setting up vuelidate

Installation

To use vuelidate in this application, the first step is to install vuelidate

npm install @vuelidate/core @vuelidate/validators

// OR

yarn add @vuelidate/core @vuelidate/validators

This command installs the two important vuelidate packages @vuelidate/core and @vuelidate/validators.

Adding to the form

To add vuelidate to our form, we need to first import the vuelidate package and vuelidate validators. Vuelidate provides a lot of built-in validators. We are going to cover some of them in this tutorial.

import {useVuelidate} from "@vuelidate/core";
import {
    required,
    email,
    minLength,
    minValue,
    sameAs,
    helpers,
    alphaNum,
} from "@vuelidate/validators";

The useVuelidate function is used to activate Vuelidate inside the component. This function accepts the validation rules and the data model. The next imports are the built-in validators that we need for this form. The next step is to define the validation rules.

const rules = {
    username: {required, minLength: minLength(3), alphaNum},
    email: {required, email},
    tel: {
        required,
    },
    age: {required, minValue: minValue(18)},
    password: {required, minLength: minLength(8)},
    confirm_password: {
        required,
        sameAs: sameAs(registrationData.password),
    },
}

This is the rules object. Its keys should match exactly those of the reactive data model. Here are explanations of the rules we defined.

  • required: This validator makes sure that the input field is not empty.
  • minLength: This is for the minimum length of strings. In the code above, the minimum length of the input cannot be less than three (3).
  • alphaNum: This validator ensures that the input contains only alphanumeric characters.
  • email: This validates email to make sure that the email is in the correct format.
  • minValue: This is for numbers, this validator makes sure the input value is not less than the defined rule. In the rule above, we don't want users below the age of 18
  • sameAs: This validator checks if two input values are the same. It accepts the value to be checked for equality.

To initialize vuelidate within this component, we create a variable v$ and set it to the useVuelidate(). The useVuelidate() function accepts two arguments the rules and the data object.

const v$ = useVuelidate(rules, registrationData);

The form is validated at the point of submission when the user submits the form. This is done using the v$.validate() function.

async function submit() {
    const isValid = await v$.value.$validate();
    if (isValid) {
        // submit form
    } else {
        // handle error
    }
}

The v$.value.$validate() is an asynchronous function which returns true or false. It returns true when the form passes the validation rules and false when the form does not meet the validation rules.


<script setup>
  import {ref, reactive, computed} from "vue";
  import {useVuelidate} from "@vuelidate/core";
  import {
    required,
    email,
    minLength,
    minValue,
    sameAs,
    helpers,
    alphaNum,
  } from "@vuelidate/validators";

  const registrationData = reactive({
    username: "",
    email: "",
    tel: "",
    age: "",
    password: "",
    confirm_password: "",
  });


  const rules = {
    username: {required, minLength: minLength(3), alphaNum},
    email: {required, email},
    tel: {
      required,
    },
    age: {required, minValue: minValue(18)},
    password: {required, minLength: minLength(8)},
    confirm_password: {
      required,
      sameAs: sameAs(registrationData.password),
    },
  }

  const v$ = useVuelidate(rules, registrationData);

  async function submit() {
    const isValid = await v$.value.$validate();
    if (isValid) {
      // submit form
    } else {
      // handle error
    }
  }
</script>

Displaying error messages

There are two ways to display error messages when using vuelidate.

  • Grouping all the error messages at the end of the form.
  • Displaying an individual error message close to the input field associated with the error message.

To group all the error messages and display them at the end of the form, we need to first understand that the errors are stored as an array of objects in the v$.errors property. To add this to the form, we would add a p element and then loop through the $error array, displaying the messages accordingly.

<p v-for="error in v$.$errors" :key="error.$uid"> {{ error.$property }} - {{ error.$message }},</p>
  • error.$uid is a unique identifier for each message.
  • error.$property shows the name of the filed where the validation failed
  • error.$message shows the error message.

![Grouped error message](res.cloudinary.com/silkydev/image/upload/v1.. "Grouped error") This is what the form would look like using the grouped error messages. This is not the best way to display errors because the messages are clustered and some users might find it difficult to locate a particular input field that has a particular error. The other way of displaying error messages is displaying individual error messages right next to the input field associated with the message. These messages are also stored as an array of objects but on the v$.${property_name}.$errors.

To display this error on the template, we would add a span element next to an input and loop through the error associated with that input field. E.g to display an error associated with the username field we would have something like this.

<span v-for="error in v$.username.$errors" :key="error.$uid">{{ error.$message }}</span>

Adding the code above for every other input field.


<main>
  <form @submit.prevent="submit">
    <div>
      <label for="username">Username</label>
      <input
          type="text"
          id="username"
          placeholder="Enter your username"
          v-model="registrationData.username"
      />
      <span v-for="error in v$.username.$errors" :key="error.$uid">
        {{error.$message }}
      </span>
    </div>
    <div>
      <label for="email">Email</label>
      <input
          type="email"
          id="email"
          placeholder="Enter your email"
          v-model="registrationData.email"
      />
      <span v-for="error in v$.email.$errors" :key="error.$uid">
        {{ error.$message }}
      </span>
    </div>
    <div>
      <label for="tel">Tel</label>
      <input
          type="tel"
          id="tel"
          placeholder="Enter your phone number"
          v-model="registrationData.tel"
      />
      <span v-for="error in v$.tel.$errors" :key="error.$uid">
        {{ error.$message }}
      </span>
    </div>
    <div>
      <label for="age">Age</label>
      <input
          type="number"
          id="age"
          placeholder="Enter your age"
          v-model="registrationData.age"
      />
      <span v-for="error in v$.age.$errors" :key="error.$uid">
        {{ error.$message}}
      </span>
    </div>
    <div>
      <label for="password">Password</label>
      <input
          type="password"
          id="password"
          placeholder="Enter your password"
          v-model="registrationData.password"
      />
      <span v-for="error in v$.password.$errors" :key="error.$uid">
        {{error.$message }}
      </span>
    </div>
    <div>
      <label for="confirm_password">Confirm password</label>
      <input
          type="password"
          id="confirm_password"
          placeholder="Confirm your password"
          v-model="registrationData.confirm_password"
      />
      <span v-for="error in v$.confirm_password.$errors" :key="error.$uid">
        {{error.$message}}
      </span>
    </div>
    <button>Submit</button>
  </form>
</main>

At this point, if the form is submitted without meeting the validation rules, error messages would be displayed next to the input field associated with them. Better error formatting

Custom Error Message

Custom error message is used to change the default error message that comes with vuelidate. This is done using the withMessage() helper function. The withMessage() helper accepts two arguments. The first is the custom message and the validator is the second.

import {helpers, required} from "@vuelidate/validators";

const rules = {
    name: {required: helpers.withMessage("Please enter your full government name", required)}
}

Custom validator

A custom validator is a function used to add a custom validation rule. These can be extra validation rules that you need but are not part of Vuelidate's built-in validators. For example, you do not want the username input field to have placeholder values like John Doe or Jane Doe.

import {helpers, required} from "@vuelidate/validators";

const checkUsername = (value) => {
    return !(value.toLowerCase() === "john doe" || value.toLowerCase() === "jane doe");
}

const rules = {
    username: {required, checkUsername: helpers.withMessage("Your cannot register with this username", checkUsername)}
}

The checkUsername function checks the username to determine if it is "john doe" or "jane doe". This function has a parameter called value that is the input value. To use the custom validator, it is used in the same way as the built-in validators. The withMessage() helper adds a custom error message.

The $dirty state

Vuelidate uses the $dirty state to check whether any input has been interacted with. The $dirty state is set to false by default until a user interacts with it.

The $dirty state can be programmatically updated using the $touch method, this validates the input and outputs proper error messages if there are any. This is done by calling v$.propertyname.touch on the input field using any input event handler.


<div>
  <label for="username">Username</label>
  <input
      type="text"
      id="username"
      placeholder="Enter your username"
      v-model="registrationData.username"
      @blur="v$.username.$touch" // touch method
  />
  <span v-for="error in v$.username.$errors" :key="error.$uid">
      {{ error.$message}}
  </span>
</div>

v$.username.$touch is called using the blur event handler. This validates the input when the user leaves the input field.

Conclusion

In this article, you learnt how to use Vuelidate to validate forms in Vue.js. You also learnt about the built-in validators that Vuelidate provides and how to define your custom validator. You can now start building and validating your forms to ensure users enter data based on the expected format. HAPPY CODING!.