Advanced Validations
We have already seen some basic validations, for example, in a previous application we have built a form like this:
myForm = 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.myForm.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:
myForm = 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 or 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
create this validator somewhere like
src/app/shared/utils/username-available-validator.ts
but it might look more
like this:
import { Injector } from '@angular/core';
import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms';
import { catchError, map, Observable, of } from 'rxjs';
import { UserService } from '../data-access/user.service';
export const usernameAvailableValidator: ValidatorFn = (
control: AbstractControl
): Observable<ValidationErrors | null> => {
const injector = Injector.create([
{ provide: UserService, useClass: UserService },
]);
return injector
.get(UserService)
.checkUsernameAvailable(control.value)
.pipe(
map((isAvailable) => (isAvailable ? null : { usernameAvailable: true })),
catchError(() => of(null))
);
};