Thanks for visiting my blog!
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:
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>
```csharp
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?