Skip to content

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 minimum
  • max — value must be less than or equal to the specified maximum
  • required — value must be a non-empty value
  • requiredTrue — value must be true
  • email — string must match an email pattern test
  • minLength — the string or array must have a length greater than or equal to the specified minimum
  • maxLength- the string or array must have a length less than or equal to the specified maximum
  • pattern — the value must match the specified regex pattern
  • nullValidator — a validator that does nothing, can be useful if you need to return a validator but don’t want it to do anything
  • compose — allows you to bundle multiple validators together into one validator
  • composeAsync — same as compose 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))
);
};

It is the same idea in general, but there are a few key differences. One big difference is that we are creating our own injector in order to inject our UserService so that we can access the checkUsernameAvailable method:

const injector = Injector.create([
{ provide: UserService, useClass: UserService },
]);
return injector
.get(UserService)

This is because we want to use the UserService which needs to be provided to us by dependency injection, but our function is not running within an “injection context” meaning that the injector that would provide us with injected dependencies is not available. Instead, we create our own injector that provides the UserService.

The alternative to this is creating our usernameAvailableValidator as an @Injectable service as well, which would make normal dependency injection available to it, but this makes it more awkward to use in our form.

Our new validator will similarly take in a control and it will return ValidationErrors or null… But! This time we return an Observable that will emit those errors or null. You can also do this with a Promise instead of an Observable.

The checkUsernameAvailable method I have set up for testing looks like this:

@Injectable({
providedIn: 'root',
})
export class UserService {
checkUsernameAvailable(username: string) {
// Randomly pass/fail
const random = Math.random();
return random < 0.5
? of(true).pipe(delay(1000))
: of(false).pipe(delay(1000));
}
}

We randomly return an observable that either emits true or false. In a real scenario, we could make a request to a server here and return true or false based on whether the username is available or not.

Our validator checks this value, and maps the response either to null or { usernameAvailable: true } based on whether the username is available or not.

Then we use it in our form like this:

myForm = this.fb.nonNullable.group({
username: ['', Validators.required, usernameAvailableValidator],
age: [null, adultValidator],
});

For context, if we had have created the validator as an @Injectable service rather than a function with that manual Injector creation stuff, we would instead have to use the validator like this:

myForm = this.fb.nonNullable.group({
username: [
'',
Validators.required,
this.usernameAvailableValidator.validate.bind(
this.usernameAvailableValidator
),
],
age: [null, adultValidator],
});

When creating a form with FormBuilder we can pass in a third option to our array for any asynchronous validators.

If we want to display the current status of the asynchronous validator in the template (e.g. whether it is currently in the process of checking) we can set up a signal like this:

usernameStatus = toSignal(this.myForm.controls.username.statusChanges);

We could then use that in the template to indicate when a check is in progress:

<input formControlName="username" type="text" />
<span>{{ usernameStatus() }}</span>

The status should render as either PENDING or VALID (you could use a pipe or some other method to change this into something more visually appealing if you like).

Creating a Confirm Password Validator

The last advanced validation situation we are going to consider is a validation that requires the values of two or more controls — this is one that we will actually be implementing in our next application.

The validators that we have created so far rely on the control being passed in:

export const adultValidator: ValidatorFn = (
control: AbstractControl
)

We can use that control however we need, like inspecting its value, to determine whether or not the control should be valid. We also supply our validator to one specific control:

myForm = this.fb.nonNullable.group({
username: ['', Validators.required, usernameAvailableValidator],
age: [null, adultValidator],
});

The adultValidator is associated with the age control. But, what about this situation:

createForm = this.fb.nonNullable.group(
{
email: ['', [Validators.email, Validators.required]],
password: ['', [Validators.minLength(8), Validators.required]],
confirmPassword: ['', [Validators.required]],
}
);

We have a form used for creating a new account, with some standard Validators like email, required, and minLength. However, let’s say we also want to verify that the password and confirmPassword fields match. If we create a custom validator and attach it to the confirmPassword control, how are we supposed to get the value of the password field to check it?

As well as supplying validators to individual controls, we can also supply them to an entire group:

createForm = this.fb.nonNullable.group(
{
email: ['', [Validators.email, Validators.required]],
password: ['', [Validators.minLength(8), Validators.required]],
confirmPassword: ['', [Validators.required]],
},
{
validators: [passwordMatchesValidator],
}
);

Now the entire group, which is still technically an AbstractControl, will be passed into the validator as the control. That validator might look like this:

import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms';
export const passwordMatchesValidator: ValidatorFn = (
control: AbstractControl
): ValidationErrors | null => {
const password = control.get('password')?.value;
const confirmPassword = control.get('confirmPassword')?.value;
return password && confirmPassword && password === confirmPassword
? null
: { passwordMatch: true };
};

This is the same as the normal custom validators we create — we just have different information available to us through the control because we are passing in the entire group. We use this to grab both the password and confirmPassword fields.

We check if both the password and confirmPassword values are present, and we also check that they are equal. If they are we return null indicating success, or we pass back an error map.

Recap

There are many different types of scenarios you might run into when trying to validate your forms, but these key concepts should be most of what you will need to know.

Remember that when doing client side validation like this, it is only for user experience. These validations are to help the user successfully fill out the form, not enforce data constraints. Any actual data validation/sanitising needs to happen on the server side — if you only rely on these client side validations they can easily be circumvented by malicious users and you could have bad data being sent to your server/being inserted into your database. The same goes for any sorts of rules we are trying to enforce on the client side, you should consider these only to be suggestions to keep good users on track — malicious users will always be able to circumvent any “security” measures we implement on the client side.