The best way to implement custom validators

Post Editor

Learning best practices on how to build your custom validator in Angular by reverse engineering the built-in Angular validators.

9 min read
0 comments
post

The best way to implement custom validators

Learning best practices on how to build your custom validator in Angular by reverse engineering the built-in Angular validators.

post
post
9 min read
0 comments
0 comments

Forms are an essential part of every Angular application and necessary to get data from the user. But, collected data is only valuable if the quality is right - means if it meets our criteria. In Angular, we can use Validators to validate the user input.

The framework itself provides some built-in validators which are great, but not sufficient for all use cases. Often we need a custom validator that is designed especially for our use case.

There are already plenty of tutorials on how to implement your custom validator. But bear with me. In this blog post, I am going to show you something different.

First, we will have a look at some built-in operators and then learn how to implement a custom validator on our own (this is what most other tutorials teach you as well). Let’s then compare the built-in operator with our custom validator. We will see that the built-in validator has some advantages over the custom validator. We will reverse engineer how Angular implements its built-in Validators and compare it with our approach. Finally, we are going to improve our implementation with the things we learned by looking at the Angular source code. Sounds good? Let’s do it!

Angular built-in validators
Link to this section

Angular provides some handy built-in validators which can be used for reactive forms and template-driven forms. Most known validators are required, requiredTrue, min, max, minLength, maxLength and pattern.

We can use these built-in validators on reactive forms as well as on template-driven forms. To validate required fields, for example, we can use the built-in required validator on template-driven forms by adding the required attribute.

<>Copy
<input id="name" name="name" class="form-control" required [(ngModel)]="hero.name">

In reactive forms, we can use it in the following way

<>Copy
const control = new FormControl('', Validators.required);

Those validators are very helpful because they allow us to perform standard form validation. But as soon as we need validation for our particular use case, we may want to provide our custom validator.

Implementing a custom validator
Link to this section

Let’s say we want to implement a simple quiz where you need to guess the right colors of the flag of a country.

Content imageContent image
Flag Quiz — Can you guess the country and the correct colors of its flag?

Do you know which country is displayed here?

Yes, it’s France. The flag of France is blue, white, and red ??. We can now use custom validators to validate that the first input field must be blue, the second one white, and the third one red.

Theoretically, we could also use the built-in pattern validators for this. But for the sake of this blogpost we are going to implement a custom validator.

Let’s go ahead and implement some custom Validators. Let’s start with the Validator that validates that we entered blue as a color.

<>Copy
import {AbstractControl, ValidatorFn} from '@angular/forms'; export function blue(): ValidatorFn { return (control: AbstractControl): { [key: string]: any } | null => control.value?.toLowerCase() === 'blue' ? null : {wrongColor: control.value}; }

The validator itself is just a function that accepts an AbstractControl and returns an Object containing the validation error or null if everything is valid. Once the blue validator is finished, we can import it in our Component and use it.

<>Copy
constructor(private fb: FormBuilder) { } ngOnInit(){ this.flagQuiz = fb.group({ firstColor: new FormControl('', blue()), secondColor: new FormControl(''), thirdColor: new FormControl('') }, {updateOn: 'blur'}); }

The firstColor input field is now validated. If it doesn’t contain the value blue our validator will return an error object with the key wrongColor and the value we entered. We can then add the following lines in our HTML to print out a sweet error message.

<>Copy
<div *ngIf="flagQuiz.get('firstColor').errors?.wrongColor" class="invalid-feedback"> Sorry, {{flagQuiz.get('firstColor')?.errors?.wrongColor}} is wrong </div>

To make our custom validator accessible for template-driven forms, we need to implement our validator as a directive and provide it as NG_VALIDATORS.

<>Copy
@Directive({ selector: '[blue]', providers: [{ provide: NG_VALIDATORS, useExisting: BlueValidatorDirective, multi: true }] }) export class BlueValidatorDirective implements Validator { validate(control: AbstractControl): { [key: string]: any } | null { return blue()(control); } }

The Directive implements the Validator interface from @angular/forms which forces us to implement the validate method.

Note the similarity of the signature of our validate function and the validate method we implemented here. They are the same. Both accept an AbstractControl and return either an error object or null.

Of course, it doesn’t make sense to duplicate the validation logic. Therefore we are going to reuse our validation function in the validate function of our Directive.

<>Copy
import { AbstractControl, NG_VALIDATORS, Validator, ValidatorFn } from '@angular/forms'; import {Directive} from '@angular/core'; export function blue(): ValidatorFn { return (control: AbstractControl): { [key: string]: any } | null => control.value?.toLowerCase() === 'blue' ? null : {wrongColor: control.value}; } @Directive({ selector: '[blue]', providers: [{ provide: NG_VALIDATORS, useExisting: BlueValidatorDirective, multi: true }] }) export class BlueValidatorDirective implements Validator { validate(control: AbstractControl): { [key: string]: any } | null { return blue()(control); } }

In our app.module.ts we can now add our Directive to the declarations and start to use it in our templates.

<>Copy
<label for="firstColor"> Enter the first color of the flag of France </label> <input #firstColor="ngModel" blue name="firstColor" class="form-control" id="firstColor" [(ngModel)]="flagQuizAnswers.firstColor" type="text"/> <div *ngIf="firstColor.errors?.wrongColor" class="invalid-feedback"> Sorry, {{firstColor?.errors?.wrongColor}} is wrong </div>

We created a custom validator that is usable within reactive forms and template-driven forms. With the same approach, we could now also implement a validator for the other colors white and red.

Let’s compare this to the Angular validators
Link to this section

Implementing the validators in this way is a valid approach. It is also the approach that you will find in most tutorials out there. Even the official Angular docs will recommend this approach. But maybe we can still do better?

Let’s compare the usage and developer experience of a built-in validator with a custom validator.

Content imageContent image
Usage of the custom validator
Content imageContent image
Usage of the built-in validator

At first glance, the usage may look very similar. But let’s take a closer look and figure out which one has the better developer experience. To do so, we judge both approaches based on the following criteria: Intellisense, consistency in whether a function call is required or not and if the Validators are grouped logically.

Content imageContent image
Comparison of built-in validators vs our custom validator

The built-in validators provide much better Intellisense than custom validators. As a developer, you don’t have to learn all the validators by heart. You just type “Validators” in your IDE and you get a list of the built-in validators. That’s not the case with our custom validator.

When using built-in validators in reactive forms, we only need to call them if we pass some additional configuration to them. (for example the pattern validator). Built-in validators follow a defined pattern. This can also be achieved with custom validators. I am not saying that this style is the correct one; it’s important to be consistent. Implement your custom validator, either “always callable” or “only callable if configurable”.

Another nice feature of the built-in validator is that they can all be accessed by using the Validators class. This allows to group relevant validators together. Our custom validators are just basic functions and not grouped. Wouldn’t it be nice if all color validators would be accessible via ColorValidators?

Reverse engineer Angular validators
Link to this section

To improve our custom validator implementation we are going to reverse engineer the built-in validators of Angular. Let’s check out how Angular implements and structures the min validator and required validator.

<>Copy
export class Validators { static min(min: number): ValidatorFn { return (control: AbstractControl): ValidationErrors|null => { if (isEmptyInputValue(control.value) || isEmptyInputValue(min)) { return null; // don't validate empty values to allow optional controls } const value = parseFloat(control.value); // Controls with NaN values after parsing should be treated as not having a // minimum, per the HTML forms spec: https://www.w3.org/TR/html5/forms.html#attr-input-min return !isNaN(value) && value < min ? {'min': {'min': min, 'actual': control.value}} : null; }; } static required(control: AbstractControl): ValidationErrors|null { return isEmptyInputValue(control.value) ? {'required': true} : null; } // ... }

Angular has a class that implements all validators as static methods. With this approach, they are “grouped” and accessible over the Validators class.

Furthermore, we can recognize a consistent pattern. The validators are “only callable if configurable”, They return a ValidatorFn for configurable validators and an error object or null for non-configurable validators.

Alright, Angular uses a static class to group validators. But how does this work with template-driven forms? Well, they use directives. Let’s have a look at the required directive.

<>Copy
@Directive({ selector: ':not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]', providers: [REQUIRED_VALIDATOR], host: {'[attr.required]': 'required ? "" : null'} }) export class RequiredValidator implements Validator { private _required = false; private _onChange?: () => void; /** * @description * Tracks changes to the required attribute bound to this directive. */ @Input() get required(): boolean|string { return this._required; } set required(value: boolean|string) { this._required = value != null && value !== false && `${value}` !== 'false'; if (this._onChange) this._onChange(); } /** * @description * Method that validates whether the control is empty. * Returns the validation result if enabled, otherwise null. */ validate(control: AbstractControl): ValidationErrors|null { return this.required ? Validators.required(control) : null; } /** * @description * Registers a callback function to call when the validator inputs change. * * @param fn The callback function */ registerOnValidatorChange(fn: () => void): void { this._onChange = fn; } }

The Directive uses a specific selector. Therefore it only works on individual form controls. The interesting part lies in the validate function. The Directive reuses the required function from the static Validators class.

Let’s summarize what we found out by looking the Angular validator source code.

  1. Angular uses a static class to group validators.
  2. Angular implements a Directive for each validator to make it accessible for template-driven forms.
  3. The validate function of the Directive reuses the function of the static class.

Let’s adapt what we have learned to our custom validator
Link to this section

Instead of implementing a standalone validation function, we are going to add it as a static field inside a ColorValdiator class.

<>Copy
import {AbstractControl, ValidatorFn} from '@angular/forms'; export class ColorValidators { static blue(control: AbstractControl): any | null { return ColorValidators.color('blue')(control); } static red(control: AbstractControl): any | null { return ColorValidators.color('red')(control); } static white(control: AbstractControl): any | null { return ColorValidators.color('white')(control); } static color(colorName: string): ValidatorFn { return (control: AbstractControl): { [key: string]: any } | null => control.value?.toLowerCase() === colorName ? null : {wrongColor: control.value}; } }

We added other validation functions called red and white.  Those color functions are not configurable and therefore directly return a error object or null. Under the hood those functions call a generic color function that verifies the value of a control against the passed color name. Notice that this function is configurable and therefore callable.

This refactoring allows us to use Intellisense and access the blue validator over ColorValidators. Furthermore we know that our validation function is not configurable and therefore we don't need to call it.

<>Copy
constructor(private fb: FormBuilder) { this.flagQuiz = fb.group({ firstColor: new FormControl('', ColorValidators.blue), secondColor: new FormControl(''), thirdColor: new FormControl('') }, {updateOn: 'blur'}); }

Provide grouped validators for template-driven forms

Currently, our grouped validator can not yet be used in template-driven forms. We need to provide a directive and then call ColorValidators inside of it.

<>Copy
import { AbstractControl, NG_VALIDATORS, Validator, ValidatorFn } from '@angular/forms'; import {Directive} from '@angular/core'; import {ColorValidators} from './color.validators'; @Directive({ selector: '[blue]', providers: [{ provide: NG_VALIDATORS, useExisting: BlueValidatorDirective, multi: true }] }) export class BlueValidatorDirective implements Validator { validate(control: AbstractControl): { [key: string]: any } | null { return ColorValidators.blue(control); } }

The usage of the validator inside a template-driven form doesn’t change.

By refactoring and restructuring our code a bit we improved the developer experience of our custom validators without loosing any features.

Conclusion
Link to this section

In the end, a custom validator is just a function that returns either an error object or null. If a validator is customizable, it needs to be wrapped with a function.

Most tutorials teach you to implement a validator as a factory function which is totally valid. However, the usage is not as nice as the built-in validators from Angular. We have no Intellisense, and if you don't follow a certain convention its not clear if a Validator needs to be called or not.

By reverse-engineering the Angular source code on validators we found an approach that shows us how to group validators. Implementing the validators as static class fields instead of standalone functions allows us to group our Validators and improve Intellisense. Furthermore, we can follow the "callable if configurable" convention. With this small refacoring we can improve developer experience.

Comments (0)

Be the first to leave a comment

Share

About the author

author_image

Google Developer Expert in Web Technologies and Angular, JavaScript enthusiast, blogger, and coach. Maintainer of multiple open source projects. Always eager to learn, share, and expand knowledge.

author_image

About the author

Kevin Kreuzer

Google Developer Expert in Web Technologies and Angular, JavaScript enthusiast, blogger, and coach. Maintainer of multiple open source projects. Always eager to learn, share, and expand knowledge.

About the author

author_image

Google Developer Expert in Web Technologies and Angular, JavaScript enthusiast, blogger, and coach. Maintainer of multiple open source projects. Always eager to learn, share, and expand knowledge.

Featured articles