DEV Community

Shawn Wildermuth
Shawn Wildermuth

Posted on • Originally published at wildermuth.com on

2

Vue 3 and Validation with the Class-Validator

ValidationI'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;
}

Enter fullscreen mode Exit fullscreen mode

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

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

Enter fullscreen mode Exit fullscreen mode

I brought in the library by:

> npm install class-validator --save

Enter fullscreen mode Exit fullscreen mode

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;

}

Enter fullscreen mode Exit fullscreen mode

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) {
    // ...
}

Enter fullscreen mode Exit fullscreen mode

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; 
  }
}

Enter fullscreen mode Exit fullscreen mode

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>

Enter fullscreen mode Exit fullscreen mode

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?

Creative Commons License

      This work by [Shawn Wildermuth](http://wildermuth.com) is licensed under a [Creative Commons Attribution-NonCommercial-NoDerivs 3.0 Unported License](http://creativecommons.org/licenses/by-nc-nd/3.0/).  
      Based on a work at [wildermuth.com](http://wildermuth.com).
Enter fullscreen mode Exit fullscreen mode

If you liked this article, see Shawn's courses on Pluralsight.

Sentry image

Hands-on debugging session: instrument, monitor, and fix

Join Lazar for a hands-on session where you’ll build it, break it, debug it, and fix it. You’ll set up Sentry, track errors, use Session Replay and Tracing, and leverage some good ol’ AI to find and fix issues fast.

RSVP here →

Top comments (0)

Sentry image

See why 4M developers consider Sentry, “not bad.”

Fixing code doesn’t have to be the worst part of your day. Learn how Sentry can help.

Learn more

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay