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.