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.

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
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.
<input id="name" name="name" class="form-control" required [(ngModel)]="hero.name">
In reactive forms, we can use it in the following way
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
Let’s say we want to implement a simple quiz where you need to guess the right colors of the flag of a country.

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.
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.
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.
<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
.
@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.
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.
<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
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.


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.

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
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.
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.
@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.
- Angular uses a
static
class to group validators. - Angular implements a
Directive
for each validator to make it accessible for template-driven forms. - The validate function of the
Directive
reuses the function of the static class.
Let’s adapt what we have learned to our custom validator
Instead of implementing a standalone validation function, we are going to add it as a static field inside a ColorValdiator
class.
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.
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.
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
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.