Shawn Wildermuth

Author, Teacher, and Filmmaker
.NET Foundation Board Member

Vue 3 and Validation with the Class-Validator

Vue 3 and Validation with the Class-Validator

I've been working with Vue 3 Beta and RC (currently in RC5) and early on I needed some validation but the Vue stalwards of vuelidate and vee-validate weren't working with the Composition API early on. What was I do to?

After some searching I ran into class-validator library. It got me thinking about how to separate the validation from the UI like I usually do in the server.

I thought I'd run you though a little example. If you want to take a look at the project, I have an example on GitHub with tags for before and after:

https://github.com/shawnwildermuth/vueclassvalidation/tags

Let's get started, first let's look at the class-validator library. For example, I have a model that my project uses that looks like this:

export default class Customer {
  id = 0;
  fullName: string | undefined;
  firstName: string | undefined;
  lastName: string | undefined;
  phoneNumber: string | undefined;
  companyName: string | undefined;
  addressLine1: string | undefined;
  addressLine2: string | undefined;
  addressLine3: string | undefined;
  cityTown: string | undefined;
  stateProvince: string | undefined;
  postalCode: string | undefined;
}

To use this, I have to make sure that the TypeScript configuration (tsconfig.json) supports decorators:

{
  "compilerOptions": {
...
    "experimentalDecorators": true,
...

I brought in the library by:

> npm install class-validator --save

After importing the decorators I applied some validations:

export default class Customer {
  id = 0;
  fullName: string | undefined;

  @MinLength(3, {
    message: "Must be > 3 characters"
  })
  firstName: string | undefined;

  @MinLength(5, {
    message: "Must be > 5 characters"
  })
  lastName: string | undefined;

  @IsOptional()
  @IsPhoneNumber("US", { message: "Must be a valid phone number" })
  phoneNumber: string | undefined;

  @IsOptional()
  @MinLength(5, {
    message: "Must be > 5 characters"
  })
  companyName: string | undefined;

  @IsDefined({
    message: "Address is required"
  })
  addressLine1: string | undefined;

  addressLine2: string | undefined;
  addressLine3: string | undefined;

  @IsDefined({
    message: "City is required"
  })
  cityTown: string | undefined;

  @IsDefined({
    message: "State is required"
  })
  @Length(2, 2, {
    message: "Must be a US State"
  })
  stateProvince: string | undefined;

  @IsDefined({
    message: "Zipcode is required"
  })
  @Matches(/^[0-9]{5}(?:-[0-9]{4})?$/, {
    message: "Must be valid Zipcode"
  })
  postalCode: string | undefined;

}

The decorators feel a lot like .NET validation. What I really like is that it's not a plugin for Vue so similar code could be used in different platforms or even in Node.

The class-validation library has a fairly simple function called validate that takes the object to validate and return a set of errors if validation fails.

let result = await validate(someObj);
for(const error of result) {
    // ...
}

To use this, I decided to make a base class for the model to check the validation on any of the models:

import { validate, ValidationError } from "class-validator";

export default abstract class BaseModel {

  public errors: Object;

  constructor() {
    this.errors = {};
  }

  public get isValid(): boolean {
    return Object.keys(this.errors).length === 0;
  }

  public async validateModel()  {
    let result = await validate(this);
    this.errors = this.setError(result)
  }

  private setError(result: ValidationError[]): Object {
    let propBag = {};

    for (const error of result) {
      for (const key in error.constraints) {
        if (Object.prototype.hasOwnProperty.call(error.constraints, key)) {
           const msg = error.constraints[key];
          (propBag as any)[error.property] = msg;
        }
      } 
    }

    return propBag; 
  }
}

This way in the view I can simply bind to the errors collection:

    <div class="form-group">
      <label for="firstName">First Name</label>
      <input class="form-control" name="firstName" v-model="customer.firstName" />
      <span
        v-if="customer.errors.firstName"
        class="text-danger small p-0 m-0"
      >{{ customer.errors.firstName }}</span>
    </div>

This snippet shows that I'm binding to the errors collection where I'd have a property per field that has an error. I flatten the errors collection a bit in the base class (see the setError function).

In this way the rules are no longer in the UI but should match the server-validation.

Any ideas on how to improve this?