An important principle that every developer should follow while coding is DRY, that is, Don't Repeat Yourself. When building applications, you should divide your codebase into small reusable pieces and call that piece of code wherever required instead of rewriting it again.
In the last guide, you learned how to validate forms in Vue using Yup. If you didn't read it, you can go through it first and come back to this one, or you can refer the code from the previous guide here.
Right now, all the form data, validations and errors are being handled inside the login form. Let's say that you have another form for registration. There again you would have to set up the validation, handle the errors, etc. So in this guide, you will be refactoring the code to make it reusable and make sure that you don't have to repeat yourself again and again for every form that you build in your application.
Extracting the methods
The first step you need to do is, extract all the methods from the form into a separate class. Create a Form
class that will accept an object of initial form values and the validation schema in its constructor. Also, inside the constructor initialize the error object, a status object and a boolean for whether or not the form is submitting.
The isSubmitting
property will be handy to give feedback to the user when the form is submitting and to disable the submit button. The status
property can store custom messages or anything you want imperatively. For example, you can use status to display any server error messages to the user.
class Form {
constructor(initialValues, validationSchema) {
this.values = initialValues;
this.validationSchema = validationSchema;
this.errors = {};
this.isSubmitting = false;
this.status = {};
}
// ...
}
Next, define few methods for validating the fields, setting the status, resetting the form, etc.
class Form {
constructor(initialValues, validationSchema) {}
validateField(field) {
// ...
}
validate() {
// ...
}
setStatus(statusField, status) {
// ...
}
setError(errorField, error) {
// ...
}
reset() {
// ...
}
submit(method, url) {
// ...
}
}
Validation methods
For validating the field, you can use the validation schema similar to what you had done in the earlier article. Whenever the validation fails, add the field error in the errors
object.
class Form {
// ...
validateField(field) {
this.validationSchema
.validateAt(field, this.values)
.then(() => (this.errors[field] = ""))
.catch(err => {
this.errors = { ...this.errors, [err.path]: err.message };
});
}
validate() {
this.validationSchema
.validate(this.values, { abortEarly: false })
.catch(err => {
err.inner.forEach(error => {
this.errors = { ...this.errors, [error.path]: error.message };
});
});
const isValid = Object.keys(this.errors).length > 0;
return isValid;
}
// ...
}
Setting The Status and Errors Imperatively
The setStatus
and setError
methods will be used to set any custom message or status on the form imperatively and to set error messages respectively from the Vue component instance.
class Form {
// ...
setStatus(statusField, status) {
this.status = { ...this.status, [statusField]: status };
}
setError(errorField, error) {
this.errors = { ...this.errors, [errorField]: error };
}
// ...
}
Form submit method
The submit()
method, will accept a form method and a URL where the form data will be sent and return a Promise
. First, check if all fields are valid; if it's invalid, then reject the Promise
. Next, make the network request using the method and the URL. Also, make sure that the isSubmitting
flag is set to true
at the beginning of the executor function, and just before rejecting or resolving the Promise
set the isSubmitting
flag to false
.
class Form {
// ...
submit(method, url) {
return new Promise((resolve, reject) => {
this.isSubmitting = true;
const isValid = this.validate(); // do client side validation
if (!isValid) {
this.isSubmitting = false;
reject({
response: {
data: {
error: "All fields are required",
},
},
});
} else
axios({ url, method, data: this.values })
.then(({ data }) => {
this.isSubmitting = false;
resolve(data);
})
.catch(err => {
this.isSubmitting = false;
this.reset();
reject(err);
});
});
}
// ...
}
Form Class Code
For your reference, you can see the completed code for Form
class below.
import axios from "axios";
export class Form {
constructor(initialValues, validationSchema) {
this.values = initialValues;
this.validationSchema = validationSchema;
this.errors = {};
this.isSubmitting = false;
this.status = {};
}
validateField(field) {
this.validationSchema
.validateAt(field, this.values)
.then(() => (this.errors[field] = ""))
.catch(err => {
this.errors = { ...this.errors, [err.path]: err.message };
});
}
setStatus(statusField, status) {
this.status = { ...this.status, [statusField]: status };
}
setError(errorField, error) {
this.errors = { ...this.errors, [errorField]: error };
}
validate() {
this.validationSchema
.validate(this.values, { abortEarly: false })
.catch(err => {
err.inner.forEach(error => {
this.errors = { ...this.errors, [error.path]: error.message };
});
});
const isValid = Object.keys(this.errors).length > 0;
return isValid;
}
reset() {
this.values = {};
this.errors = {};
}
submit(method, url) {
return new Promise((resolve, reject) => {
this.isSubmitting = true;
const isValid = this.validate(); // do client side validation
if (!isValid) {
this.isSubmitting = false;
reject({
response: {
data: {
error: "All fields are required",
},
},
});
} else
axios({ url, method, data: this.values })
.then(({ data }) => {
this.isSubmitting = false;
resolve(data);
})
.catch(err => {
this.isSubmitting = false;
this.reset();
reject(err);
});
});
}
}
Using the Form
class inside the component
Now, it's time to use the Form
class inside the Vue component. Create a new Form instance inside the data()
method and all you need to do is pass the initial form data and the validation schema. That's it, all the complexities around validation is handled by the Form
class.
const loginFormSchema = object().shape({
email: string().email().required(),
password: string().required(),
});
export default {
name: "app",
components: {
"form-input": FormInput,
},
data() {
return {
form: new Form(
{
email: "",
password: "",
},
loginFormSchema
),
};
},
methods: {
loginUser() {
const URL = "https://some-foo-url/login"; // your login API endpoint
this.form
.submit("post", URL)
.then(data => {
console.log(data.user.token); // store the login token
this.form.setStatus("success", "User logged in successfully!");
// redirect the user
})
.catch(err => {
if (err.response)
this.form.setStatus("error", err.response.data.error);
});
},
},
};
Using this Form
class, you can create forms with ease, without having to worry about validation. You can add more methods to this class that better suits the needs of your application.
That was it from this guide. You can check out the entire code in this repo.