Form validation using AJV, Vue.js and TypeScript

Form validation using AJV, Vue.js and TypeScript

Form validation is an important part of front-end development that helps improve the user experience and prevent errors when sending data to the server. In this article, we’ll look at how to use the AJV library in conjunction with Vue.js and TypeScript to create a powerful form validation system.

What is AJV?

AJV (Another JSON Schema Validator) is a fast JSON data validation library with JSON Schema support. JSON Schema is a language for describing the structure and validation of data in JSON format. AJV allows you to check data against prepared validation schemes.

Project preparation

Before you begin, make sure you have:

  1. Node.js v18.16.1.

  2. @vue/cli 5.0.8

Let’s create a new project using the Vue CLI with the following parameters:

vue create ajv-validation

Let’s install the necessary dependencies in our project:

npm install ajv ajv-formats ajv-errors

Creating the login.json validation scheme file

login.json
{
  "$id": "/login.json",
  "type": "object",
  "additionalProperties": false,
  "required": ["login", "password"],
  "properties": {
    "login": {
      "type": "string",
      "format": "email",
      "errorMessage": "enter a valid email address"
    },
    "password": {
      "type": "string",
      "minLength": 6,
      "maxLength": 1024
    }
  }
}

In this schema, we define the type of each field (string), and also set some validation rules, such as email format and minimum password length. The “login” and “password” fields are mandatory.

Let’s briefly go through 2 important methods:

onLogin is a method that is called when a user attempts to log in to the system (login). It checks and validates the data entered by the user and takes appropriate actions depending on the result of the check.

  1. Checking the entered data for correctness using the validate function. This function uses a data validation scheme and checks that the data conforms to this scheme. The isValid flag is returned to indicate whether the validation was successful.

  2. If the data is invalid (isValid is false), error handling is performed.

function onLogin()
function onLogin() {
      errors.value.clear();
      if (validator.validate("/login.json", formData.value)) {
        // valid, do nothing
      } else if (validator.errors?.length) {
        for (const [, e] of validator.errors.entries()) {
          if (!e.message) {
            continue;
          }
          const fieldName = e.instancePath.substring(1);
          const fieldErrors: string[] = errors.value.get(fieldName) || [];
          fieldErrors.push(e.message);
          errors.value.set(fieldName, fieldErrors);
        }
      }
    }

onBlur is a method that is called upon the “blur” event (loss of focus) on the form’s input text field. It is used to validate user input when the user moves from a field to another form element or clicks outside a text field.

  1. Get the name (id) and value of the input field on which the “blur” event occurred.

  2. Get the validation scheme for this field.

  3. Checking the field value for correctness using the validate function.

  4. If the field value is incorrect (isValid is false), error handling is performed.

function onBlur()
function onBlur(e: any) {
      const fieldName = e.target.id;
      const fieldValue = e.target.value;
      if (
        validator.validate(`/login.json#/properties/${fieldName}`, fieldValue)
      ) {
        errors.value.delete(fieldName);
      } else if (validator.errors?.length) {
        errors.value.set(
          fieldName,
          validator.errors.map((e) => e.message) as string[]
        );
      }
    }

Conclusion

Now you have an example of how to use AJV with Vue.js and TypeScript for form validation. This allows you to create powerful and flexible validation systems that will help improve the user experience and ensure correct data processing on the server.

Source code

App.vue
<template>
  <div class="login-form">
    <h2>Login</h2>
    <form @submit.prevent="onLogin">
      <div class="form-group">
        <label for="login">Email</label>
        <input
          v-model="formData.login"
          :class="{ 'is-invalid': isInvalid(errors.has('login')) }"
          @blur="onBlur"
          type="text"
          id="login"
          placeholder="Enter your email"
        />
        <ul class="error-wrapper">
          <li v-for="errorMsg of errors.get('login')" :key="errorMsg">
            {{ errorMsg }}
          </li>
        </ul>
      </div>

      <div class="form-group">
        <label for="password">Password</label>
        <input
          v-model="formData.password"
          :class="{ 'is-invalid': isInvalid(errors.has('password')) }"
          @blur="onBlur"
          type="password"
          id="password"
          placeholder="Enter your password"
        />

        <ul class="error-wrapper">
          <li v-for="errorMsg of errors.get('password')" :key="errorMsg">
            {{ errorMsg }}
          </li>
        </ul>
      </div>

      <button type="submit" :disabled="errors.size > 0" @click="submit('ok')">
        Login
      </button>
    </form>
  </div>
</template>

<script lang="ts">
import { defineComponent, ref, watch } from "vue";
import login from "@/schemas/login.json";
import Ajv from "ajv";
import ajvFormats from "ajv-formats";
import ajvErrors from "ajv-errors";

export default defineComponent({
  setup() {
    const formData = ref({
      login: "",
      password: "",
    });

    const opts = { allErrors: true };
    const validator = ajvErrors(ajvFormats(new Ajv(opts)));
    validator.addSchema(login);

    let errors = ref<Map<string, string[]>>(new Map());
    watch(
      () => errors.value,
      () => void 0
    );

    function onBlur(e: any) {
      const fieldName = e.target.id;
      const fieldValue = e.target.value;
      if (
        validator.validate(`/login.json#/properties/${fieldName}`, fieldValue)
      ) {
        errors.value.delete(fieldName);
      } else if (validator.errors?.length) {
        errors.value.set(
          fieldName,
          validator.errors.map((e) => e.message) as string[]
        );
      }
    }

    function onLogin() {
      errors.value.clear();
      if (validator.validate("/login.json", formData.value)) {
        // valid, do nothing
      } else if (validator.errors?.length) {
        for (const [, e] of validator.errors.entries()) {
          if (!e.message) {
            continue;
          }
          const fieldName = e.instancePath.substring(1);
          const fieldErrors: string[] = errors.value.get(fieldName) || [];
          fieldErrors.push(e.message);
          errors.value.set(fieldName, fieldErrors);
        }
      }
    }

    function formColor(error: any) {
      return error?.length;
    }

    function submit(text: string) {
      alert(text);
    }

    return {
      formData,
      errors,
      isInvalid: formColor,
      onBlur,
      onLogin,
      submit,
    };
  },
});
</script>

<style>
body {
  font-family: sans-serif;
}

.login-form {
  max-width: 300px;
  margin: 0 auto;
  padding: 20px;
  border: 1px solid #ccc;
  border-radius: 5px;
  background-color: #f9f9f9;
}

.login-form h2 {
  text-align: center;
  margin-bottom: 20px;
}

.form-group {
  margin-bottom: 20px;
}

label {
  display: block;
  font-weight: bold;
  margin-bottom: 5px;
}

input {
  width: 100%;
  padding: 8px;
  border: 1px solid #ccc;
  border-radius: 5px;
}

button {
  width: 100%;
  padding: 10px;
  background-color: #007bff;
  color: #fff;
  border: none;
  border-radius: 5px;
  cursor: pointer;
  transition: background-color 0.3s ease;
}

button:hover {
  background-color: #0056b3;
}

button:disabled {
  background-color: #ccc;
}

.is-invalid {
  border: 1px solid red;
}

.error-wrapper {
  color: red;
  font-size: 12px;
  margin: 0;
  padding: 0 20px;
}
</style>

github

Related posts