Reusable And Declarative Way Of Handling Forms in Vue

Mon Jul 13 2020

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.

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