Form Validation in VueJS using Yup

Fri May 15 2020

Admit it, handling form validations is a pain when it comes to JavaScript frameworks. Although, form validation is available natively in the browser, there are still some gotchas when it comes to cross-browser compatibility. There are many validation libraries you might want to choose from, but don't know how to get started.

VueJS ecosystem has form validation libraries like vuelidate and VeeValidate. These libraries are great, but I would like to show you an alternative way of handling validations using a schema-based validation library called yup.

Before getting started, note that this demo uses spectre.css framework for styling the form, but you can do the same with pretty much all the CSS frameworks.

Open up your favourite text editor and let's get started!

Initial Form

For this demo, we will be working with a Login Form. The form will have two fields; email and password. Each field will have its v-model value. Use the @submit.prevent directive to prevent the default form submission behavior and call the loginUser() method.

<template>
  <div id="app">
    <form class="login-form" @submit.prevent="loginUser">
      <h2>Login</h2>
      <div class="form-group">
        <label class="form-label" for="email">Email</label>
        <input
          id="email"
          name="email"
          type="email"
          v-model="email"
          class="form-input"
        />
      </div>
      <div class="form-group">
        <label class="form-label" for="email">Password</label>
        <input
          id="password"
          name="password"
          type="password"
          v-model="password"
          class="form-input"
        />
      </div>
      <button class="btn btn-primary btn-block">Login</button>
    </form>
  </div>
</template>
export default {
  name: "app",
  data() {
    return {
      email: "",
      password: "",
    };
  },
  methods: {
    loginUser() {
      console.log("Logging In...");
    },
  },
};

Installing and configuring yup

Yup is an Object schema based validation and parsing library. It can define a schema, validate the shape of an existing object, transform a value to match, or both. The feature I like the most about Yup is that it has a straightforward API which makes it very easy to use.

Now that you have the basic form set up; install yup using yarn or npm.

yarn add yup

Either you can import the required stuff from yup, or everything as shown below.

import { object, string } from "yup";

// to import everything from yup
import * as yup from "yup";

Creating the validation Schema

To define an object schema, use the object() method and call the shape() method attached to it with your schema.

const loginFormSchema = object().shape({
  // schema
});

Or you could just pass your schema in the object() method.

const loginFormSchema = object({
  // schema
});

Since you have string based validations, use the string() method to define a string schema.

So for the email and password fields it will be,

{
  email: string().required().email(),
  password: string().required()
}

So putting it all together, your final validation schema should look as follows.

const loginFormSchema = object().shape({
  email: string().required().email(),
  password: string().required(),
});

You can add more validation methods like max() and min() on the password field, I'm leaving that up to you. You can check out all the other validation methods in the Yup repo.

Adding Validation

You need to trigger the validation when the user is typing in the field, or when the field loses focus. So use the @keypress and @blur event directives on the inputs and call the validate() method with the field name.

<input
  id="email"
  name="email"
  type="email"
  v-model="values.email"
  class="form-input"
  @blur="validate('email')"
  @keypress="validate('email')"
/>
<!-- -->
<input
  id="password"
  name="password"
  type="password"
  v-model="values.password"
  class="form-input"
  @blur="validate('password')"
  @keypress="validate('password')"
/>

Refactor the data object to include the error messages as well.

export default {
  name: "app",
  data() {
    return {
      values: {
        email: "",
        password: "",
      },
      errors: {
        email: "",
        password: "",
      },
    };
  },
  methods: {
    // ...
  },
};

Now, let's define the validate method. The validate method, as the name suggests, will validate a single field against the validation schema. You can do that by using the validateAt() method of the validation schema.

The validateAt() method returns a Promise, so you can catch all the validation errors in the catch callback. If you console log the err object, you will find the error message in the message property.

// ...
methods: {
    loginUser() {
      // ...
    },
    validate(field) {
      loginFormSchema
        .validateAt(field, this.values)
        .catch(err => {
          console.log(err);
          /*
            {
              errors: ["email is a required field"],
              inner: [],
              message: "email is a required field",
              name: "ValidationError",
              params: {path: "email", value: "", originalValue: "", label: undefined},
              path: "email",
              type: "required",
              value: "",
              // ..
            }
          */
        });
    },
}
// ...

When the validation fails assign the error message to the property for that field in the errors object. For example, if there's an error in the email field, assign the error message to this.errors.email. You also need to clear the error message from the errors object if there's no error.

// ...
methods: {
    loginUser() {
      // ...
    },
    validate(field) {
      loginFormSchema
        .validateAt(field, this.values)
        .then(() => {
          this.errors[field] = "";
        })
        .catch(err => {
          this.errors[field] = err.message;
        });
    },
}
// ...

The loginUser() method will be called when the user clicks on the submit button. Here, you need to validate both the fields using the validate() method. By default the validate() method will reject the promise as soon as it finds the error and wont validate any further fields. So to avoid that you need to pass the abortEarly option and set the boolean to false { abortEarly: false }.

methods: {
    loginUser() {
      loginFormSchema
        .validate(this.values, { abortEarly: false })
        .then(() => {
          this.errors = {};

          // login the user
        })
        .catch(err => {
          err.inner.forEach(error => {
            this.errors[error.path] = error.message;
          });
        });
    },
// ...
}

Rendering errors on the form

Now that you are storing the error messages, let's see how you can render them in the form. This next part will depend on the CSS framework that you are using. Spectre uses the has-error class to change the input's border color to show the error.

For the email field, you can check if there's any message in the errors.email field and add the has-error class.

<div :class="[ 'form-group', !!errors.email && 'has-error' ]">
  <!-- -->
</div>

And then you can render the message after the input as shown below.

<!-- After the input -->
<p class="form-input-hint" v-if="!!errors.email">
  {{ errors.email }}
</p>

Similarly, you can do for the password field as well.

<div :class="[ 'form-group', !!errors.password && 'has-error' ]">
  <label class="form-label" for="password">Password</label>
  <input
    id="password"
    name="password"
    type="password"
    v-model="values.password"
    class="form-input"
    @blur="validate('password')"
    @keypress="validate('password')"
  />
  <p class="form-input-hint" v-if="!!errors.password">
    {{ errors.password }}
  </p>
</div>

Refactoring inputs

As you can see above, the input field has got a lot of templates that can be abstracted by creating it as a separate component. So, let's do that and pass the type, label, name, value and error as props.

export default {
  name: "form-input",
  props: {
    type: { required: true },
    label: { required: true },
    name: { required: true },
    value: { required: true },
    error: { required: true },
  },
};

To implement the v-model directive in a custom component, you need to specify the value prop and the input event in the input tag inside the custom component.

For more info, you can check out this guide in the official VueJS site and further refactor the component, but for brevity purpose, I'm skipping that for now.

<template>
  <div :class="[ 'form-group', !!error && 'has-error' ]">
    <label class="form-label" for="email">{{label}}</label>
    <input
      :id="name"
      :name="name"
      :type="type"
      :value="value"
      @input="$emit('input', $event.target.value)"
      class="form-input"
      @blur="$emit('validate')"
      @keypress="$emit('validate')"
    />
    <p class="form-input-hint" v-if="!!error">{{ error }}</p>
  </div>
</template>

After you are done with the custom input component, you can use it inside the form as shown below.

<form-input
  label="Email"
  v-model="values.email"
  type="email"
  @validate="validate('email')"
  name="email"
  :error="errors.email"
></form-input>

You can find the entire code for this blog in this repo.

Conclusion

Well, that was simple right? Form validation on the frontend can be a pain, but Yup makes it super easy for us to handle custom form validations.

Now that validation is dealt with, in the next part I'll show you how to create a reusable and declarative form API to handle the form values and errors so that you don't have to repeat yourself again and again for each form in your application.

If you read this far, and liked the post, please support and share.

🤙 Get in touch with me

This site is built with Gatsby and powered by Netlify