3d cartoon hands holding a phone

Unlock full course by purchasing a membership

Lesson 3

Advanced Validations

Creating custom validators for controls

EXTENDED

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 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 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))
    );
  }
}
EXTENDED
Key

Thanks for checking out the preview of this lesson!

You do not have the appropriate membership to view the full lesson. If you would like full access to this module you can view membership options (or log in if you are already have an appropriate membership).