Advanced Validations
We have already seen some basic validations, for example, in a previous application we have built a form like this:
checklistForm = this.fb.nonNullable.group({
title: ['', Validators.required],
});
The general idea here is that if any of the Validators
fail against the input for that control, then the valid
property of the form will be false
, e.g:
this.checklistForm.valid
We can also check the validity of just that individual control as well:
this.myForm.controls.title.valid
We can also use multiple validators for a single field by providing an array of validators:
checklistForm = this.fb.nonNullable.group({
title: ['', [Validators.required, Validators.minLength(5)]],
});
By default, Angular comes with a range of validators that suit most cases:
min
- value must be greater than or equal to the specified minimummax
- value must be less than or equal to the specified maximumrequired
- value must be a non-empty valuerequiredTrue
- value must be trueemail
- string must match an email pattern testminLength
- the string or array must have alength
greater than or equal to the specified minimummaxLength
- the string or array must have alength
less than or equal to the specified maximumpattern
- the value must match the specified regex patternnullValidator
- a validator that does nothing, can be useful if you need to return a validator but don’t want it to do anythingcompose
- allows you to bundle multiple validators together into one validatorcomposeAsync
- same ascompose
but allows for asynchronous validations
But, sometimes this is not enough and we might require a validator that is custom made for our specific situation. We are going to cover a couple of advanced techniques for validation in this lesson. We will cover how to create our own asynchronous validator, and also our own custom validator that performs a validation across multiple fields within the form.
Custom Validators
Creating our own custom validators is actually reasonably easy - the general idea is we create a function that accepts a control
and we return null
if the validation passes or we return a map of validation errors if it fails.
As an example, we might create a custom validator at src/app/shared/utils/adult-validator.ts
that looks like this:
import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms';
export const adultValidator: ValidatorFn = (
control: AbstractControl
): ValidationErrors | null => {
return control.value >= 18 ? null : { adultValidator: true };
};
We have the control
passed in and we access its value
. We check if that value
is greater than of equal to 18
. If it is, we return null
which means the validation has passed. If it is not, we return an object indicating the validation errors, which is:
{
adultValidator: true
}
I think this can be kind of confusing, because providing this map of errors kind of reads as if the adultValidator
passed because it is listed as true
. But really, this is saying that there was an error with adultValidator
.
We can then import and use this custom validator in our form like any other validator, e.g:
myForm = this.fb.nonNullable.group({
title: ['', Validators.required],
age: [null, adultValidator],
});
This is a simple example, and really we could just use the min
validator for this, but we will look at more advanced implementations in the next two examples.
An Asynchronous Validator
A great example of an asynchronous validator is validating whether or not a given username
has been taken or not. Most validators work synchronously - we instantly know if they are valid or not. But for something like this, we would need to make a request to a server with the value provided to check it, and that is going to take some time.
The validator we would create for this situation is a little different. We could also create this validator somewhere like src/app/shared/utils/username-available-validator
but it might look more like this:
import { Injectable } from '@angular/core';
import {
AbstractControl,
AsyncValidator,
ValidationErrors,
} from '@angular/forms';
import { catchError, map, Observable, of } from 'rxjs';
import { UserService } from '../data-access/user.service';
@Injectable({
providedIn: 'root',
})
export class UsernameAvailableValidator implements AsyncValidator {
constructor(private userService: UserService) {}
validate(control: AbstractControl): Observable<ValidationErrors | null> {
return this.userService.checkUsernameAvailable(control.value).pipe(
map((isAvailable) => (isAvailable ? null : { usernameAvailable: true })),
catchError(() => of(null))
);
}
}