Angular Custom Form Validators Complete Guide

When building large scale forms with the Angular Forms module, the available built-in validators (such as making a field required, etc.) usually won't be sufficient, and so you will have to develop your own custom form validation rules.

In this post, you will learn everything that you need to know in order to implement your own custom form validators, including both synchronous and asynchronous, field-level, form-level, and both for template-driven and reactive forms.

What is a form field Validator?

If you have used either the Angular Forms or ReactiveForms modules, you will have noticed that every form has a couple of important properties:

  • the form value, consisting of the values of all the individual form fields
  • a form validity state, which is true if all the form fields are valid, and false if at least one of the forms fields is invalid

Each form field has its own set of business validation rules: the fields can be mandatory, have a minimum length of 10 characters, etc.

Here is an example of a simple reactive form, with a couple of standard validators:

<form [formGroup]="form">
  <input
    matInput
    type="email"
    name="email"
    placeholder="Email"
    formControlName="email"
  />
 
  <input
    matInput
    type="password"
    placeholder="Password"
    formControlName="password"
  />
 
  <button [disabled]="!form.valid">Login</button>
</form>

As we can see, this is a simple login form with an email and password fields. We don't see any business validation rules because this is a reactive form, so all validation rules are defined on the component, and not on the template.

Here is the corresponding component class:

@Component({
  selector: "login",
  templateUrl: "./login.component.html",
  styleUrls: ["./login.component.css"],
})
export class LoginComponent {
  form = this.fb.group({
    email: [
      "",
      {
        validators: [Validators.required, Validators.email],
        updateOn: "blur",
      },
    ],
    password: [
      "",
      [
        Validators.required,
        Validators.minLength(8),
        createPasswordStrengthValidator(),
      ],
    ],
  });
 
  constructor(private fb: FormBuilder) {}
 
  get email() {
    return this.form.controls["email"];
  }
 
  get password() {
    return this.form.controls["password"];
  }
}

There are a lot of validation rules being used here. Let's go through them one at a time, starting with the simpler ones.

In this particular reactive form, we are using the FormBuilder API in order to define our form fields and validation rules in a concise way.

For example, the email field is marked as mandatory via the Validators.required validator, and it also needs to follow the format of a valid email, due to the use of the Validators.email validator.

These are two examples of the several built-in validators that we have available to use, right out of the box.

So how do these validators work, what is a validator?

A form field validator is a function that the form can call in order to decide if a given form field is valid or not.

A validator function returns true if the form field is valid according to the validator rules, or false otherwise.

A validator can be plugged in directly into a reactive form simply by adding it to the validators list. Each form field has its own list of separate validators.

Two alternative syntaxes for configuring validators

If you noticed, the validation rules for the email and the password fields were configured in a slightly different way.

The password field was configured in the way that you are probably the most used to, by using the FormBuilder API simplified array syntax:

 form = this.fb.group({
    password: [
        <form initial value>,
        [ ... array of synchronous validators ...]
    ]
 });

With this syntax, each form field has an array containing both the initial value of the form field and its list of synchronous validators.

Although this syntax is compact, it might become a bit difficult to read for fields with a larger number of validators.

An alternative syntax is the one used by the email field:

 form = this.fb.group({
    email: ['', {
        validators: [ ... array of synchronous validators ...],
        asyncValidators: [ ... array of asynchronous validators ...]
        updateOn:  'change' or 'blur' or 'submit'
    }],
    ...
 });

In this alternative and more powerful syntax, but also slightly more verbose, the second value of the configuration array (after the initial form field value) is a configuration object containing 3 properties that we can optionally use:

  • **validators:**contains the list of synchronous validators for the form field
  • **asyncValidators:**contains the list asynchronous validators for the form field (not used in this example)
  • **updateOn:**specifies the moment when the form should be updated with the new form field value and recalculate the field validity (more on this later).

Our advice is to switch to the more powerful syntax with the configuration object as soon as you need to, either to set the updateOn property or to make the form field definition more readable.

Notice also, in the login reactive form, that next to the standard built-in required and minLength validators of the password field, we have a non-standard validator createPasswordStrengthValidator().

This is a custom validator, that we have written ourselves.

Custom Validators for Reactive Forms

The password strength validator ensures that the password has enough strength besides a minimum length. The password strength might be used to require the password to contain not only lowercase characters but also upper case and numeric characters, for example.

With reactive forms, writing a custom validator is as easy as writting a new function:

import { AbstractControl, ValidationErrors, ValidatorFn } from "@angular/forms";
 
export function createPasswordStrengthValidator(): ValidatorFn {
  return (control: AbstractControl): ValidationErrors | null => {
    const value = control.value;
 
    if (!value) {
      return null;
    }
 
    const hasUpperCase = /[A-Z]+/.test(value);
 
    const hasLowerCase = /[a-z]+/.test(value);
 
    const hasNumeric = /[0-9]+/.test(value);
 
    const passwordValid = hasUpperCase && hasLowerCase && hasNumeric;
 
    return !passwordValid ? { passwordStrength: true } : null;
  };
}

Let's break down what is going on here. As we can see, the createPasswordStrengthValidator() function is not the validator function itself.

This is actually a validator creation function, that returns as its output the validator. So it's a function that returns a function, as we can see this by its return type ValidatorFn.

The validator creation function can have any arguments needed to set up the validator (in this case no arguments were needed), and returns as the output the validator function itself.

The validator function is then going to be called by the form in order to determine if the form field value is valid or not.

And as we can see, the validator function needs to follow certain conventions.

How to write a Validator function

The form field validator function, returned by the validator creation function, needs to follow these rules:

  • only one input argument is expected, which is of type AbstractControl. The validator function can obtain the value to be validated via the control.value property
  • The validator function needs to return null if no errors were found in the field value, meaning that the value is valid
  • If any validation errors are found, the function needs to return an object of type ValidationErrors
  • The ValidationErrors object can have as properties the multiple errors found (usually just one), and as values the details about each error.
  • The value of the ValidationErrors object can be an object with any properties that we want, allowing us to provide a lot of useful information about the error if needed
  • If we just want to indicate that an error occurred, without providing further details, we can simply return true as the value of an error property in the ValidationErrors object

In the particular case of our password strength validator function, we are checking for the presence of all required characters, and in the end, we are either:

  • returning null if the password is valid and no errors were found
  • returning {passwordStrength:true} as an output error object, in case that the password is not strong enough

As we can see, we are only returning a boolean flag in the error object indicating that an error was found, without more details. But we could instead also return an error object similar to this one:

{
  "passwordStrength": {
    "hasUpperCase": true,
    "hasLowerCase": true,
    "hasNumeric": false
  }
}

This ValidationErrors object can have any form that we need. We can return nested objects if we need to, in order to convey all the necessary information about what caused the error.

Once we have a new custom validator function written, all we have to do is plug it in our reactive form:

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

Let's remember, the createPasswordStrengthValidator() function did not need input arguments, but we could have defined any arguments that we needed in order to perform the validation, and pass them in this function call.

Displaying error messages for custom Validators

In order to inform the user that there is a problem with the password strength, we can add the following message to the template:

<input
  matInput
  type="password"
  placeholder="Password"
  formControlName="password"
/>
 
<div class="field-message" *ngIf="password.errors?.passwordStrength">
  Your password must have lower case, upper case and numeric characters.
</div>

As we can see, the ValidationErrors object that we returned as the output of our custom validator was added to the pasword field errors object.

If the password is not strong enough, the ngIf condition will turn to true and the error message will then be shown to the user.

Custom Validators for Template-Driven Forms

For template-driven forms, it takes a bit more work to define a custom form field validator.

With template driven forms, all the business validation rules are defined at the level of the template using directives, and not at the level of the component class.

For example, here is what the same login form would look like in its template-driven version:

<form #loginForm="ngForm" (ngSubmit)="login(loginForm, $event)">
 
    <input matInput type="email" name="email"
           ngModel
           #email="ngModel"
           required email minlength="3" maxlength="20"
           placeholder="Email">
 
    <input matInput type="password" name="password"
           required passwordStrength minlength="8"
           ngModel #password="ngModel"
           placeholder="Password">
 
  </mat-form-field>
 
  <button type="submit" [disabled]="!loginForm.valid">
    Login
  </button>
 
</form>

As we can see, in the case of template-driven forms, there is a lot more going on in the template.

For example, we can see the standard required, minlength, maxlength and email directives applied to the email and password fields.

The way that these directives work, is that they use internally the corresponding validator functions Validators.required, Validators.minlength, and Validators.maxLength.

Notice also the non-standard passwordStrength directive, applied to the password field. This one we will have to build ourselves!

So in the case of template-driven forms, besides the validator function, we also need to create a custom directive to go along with it.

Otherwise, we won't be able to use our custom validators in our template-driven forms.

How to write a custom Validator directive

Let's have a look at what the custom form field passwordStrength directive looks like:

import { Directive } from "@angular/core";
import {
  AbstractControl,
  NG_VALIDATORS,
  ValidationErrors,
  Validator,
} from "@angular/forms";
import { createPasswordStrengthValidator } from "../validators/password-strength.validator";
 
@Directive({
  selector: "[passwordStrength]",
  providers: [
    {
      provide: NG_VALIDATORS,
      useExisting: PasswordStrengthDirective,
      multi: true,
    },
  ],
})
export class PasswordStrengthDirective implements Validator {
  validate(control: AbstractControl): ValidationErrors | null {
    return createPasswordStrengthValidator()(control);
  }
}

Let's break this down step by step, starting with the implementation of the validation functionality itself:

  • in order to implement a custom validation directive, we need to implement the Validator interface, which only has the validate method
  • the validate method is going to call the validation creation function, and pass the control reference (the password control in this case) to the validator
  • the validator is going to return null if the password valid is false, on a ValidationErrors error object otherwise

But implementing the Validator interface is not enough to make this a valid custom validation directive. We also have to configure it properly in the Angular dependency injection system.

Understanding the DI configuration of a custom validator directive

The Angular Forms module needs to know what are the multiple custom validation directives available. It does so by asking the dependency injection system for the NG_VALIDATORS injection token (a sort of unique dependency injection key, that uniquely identifies a dependency).

This dependency in of type multi:true meaning that it includes multiple values, and not just one value. This is to be expected, as there are many custom validator directives, and not just one.

We can see in our directive declaration that we are creating a new provider, meaning that we are setting up a new value in the DI system and we are making the PasswordStrengthDirective available to be instantiated by the DI system if needed.

Because the NG_VALIDATORS provider is of type multi:true, we are adding the PasswordStrengthDirective to the list of existing custom validation directives, instead of replacing the new list with a single value.

Notice that adding the proper DI configuration to your custom validation directive is essential for it to work. Without this, the validate() method simply won't get triggered.

Comparing custom Validation in template-driven vs reactive forms

As we can see, it takes a bit more work and also more general Angular knowledge to implement custom validation in template-driven forms.

With reactive forms, all we have to do is to write a function. But in template-driven forms, besides implementing a Validator function, we also have to know how to write our own custom directive and know how to configure the dependency injection system.

But even though things are a bit harder in template-driven forms, it's still quite doable.

If you want to learn more about the differences between template-driven and reactive forms, you can check out our previous article.

Form-level (multi-field) Validators

Besides being able to define custom validators at the level of each field, we can also define custom validators at the level of the form.

This is very useful in order to do for example multi-field validation, when a validation rule can be true or false depending on the values of multiple different form fields, as opposed to just one field.

Here is an example, imagine a reactive form that takes in two separate input dates, a start date and an end date:

<form [formGroup]="form">
  <input placeholder="Start date" formControlName="startAt" />
 
  <input placeholder="End date" formControlName="endAt" />
</form>

In order for the form to be considered valid, it's not enough to make the two dates mandatory. We also need to put in place a validation rule that says that the start date should be before the end date.

And this can't be done with a custom validator at the level of the form field. Instead, we need to define a validator at the level of the form.

And we can do so also by defining a custom validator function, in a similar way to field-level validators.

Here is an example of a form-level validator for checking if the start date is before the end date:

import { FormGroup, ValidatorFn, Validators } from "@angular/forms";
 
export function creatDateRangeValidator(): ValidatorFn {
  return (form: FormGroup): ValidationErrors | null => {
    const start: Date = form.get("startAt").value;
 
    const end: Date = form.get("endAt").value;
 
    if (start && end) {
      const isRangeValid = end.getTime() - start.getTime() > 0;
 
      return isRangeValid ? null : { dateRange: true };
    }
 
    return null;
  };
}

As we can see, defining a form-level validator is almost the same as a field-level validator! The only difference is that the input of the function is a FormGroup object instead of a form control.

Using the FormGroup, we can easily access any value of the form and compare multiple values together, like for example the start date and end date fields.

Once the form-level validator function is written, all we have to do it is to apply it in the configuration of the form:

form = this.fb.group(
  {
    startAt: [null, Validators.required],
    endAt: [null, Validators.required],
  },
  {
    validators: [createDateRangeValidator()],
  }
);

As we can see, this type of validator needs to be configured at the level of the form group as a separate configuration object, and not at the level of any of the form fields.

This form-level configuration object can also take the asyncValidators and updateOn property, which we will cover in a moment.

Asynchronous form field Validators

All of the custom validators that we have been showing so far are synchronous, meaning that the new validity state is immediately calculated by the validator as soon as the validator function is called.

For the case of simple validators like mandatory, minimum length, password strength etc. this makes sense, but there are situations where we would like for this new validity state to be calculated in an asynchronous way.

Imagine that in order to determine the new validity state, we would need to perform some sort of asynchronous operation, such as for example calling a backend or using any sort of promise-based library.

In that case, synchronous validators would simply not work. One such example would be the case of the email field. In the case of a user creation form, we want the form button to be enabled only if the user email is not yet used on the backend.

In order to check if the user already exists, we need to call the backend and query the database, which requires an asynchronous operation, and that's where asynchronous validators come in.

Here is what our user creation form would look like:

<form [formGroup]="form">
  <input
    matInput
    type="email"
    name="email"
    placeholder="Enter the new user email"
    formControlName="email"
    #email
  />
 
  <div class="field-message" *ngIf="email?.errors.userExists">
    An user with email {{email.value}} already exists.
  </div>
 
  <input
    matInput
    type="password"
    placeholder="Password"
    formControlName="password"
  />
 
  <input
    matInput
    type="password"
    placeholder="Confirm Password"
    formControlName="confirmPassword"
  />
 
  <button [disabled]="!form.valid">Create User</button>
</form>

As we can see, an error message will be shown to the user informing that an user with that email already exists, if the property userExists is present at the level of the field errors object.

Let's now see what the component class looks like, focusing on the email field only:

@Component({
  selector: 'login',
  templateUrl: './create-user.component.html',
  styleUrls: ['./create-user.component.css']
})
export class CreateUserComponent {
 
   form = this.fb.group({
      email: ['', {
          validators: [
             Validators.required,
             Validators.email
          ],
          asyncValidators: [userExistsValidator(this.user)]
          updateOn: 'blur'
      }],
      ....
   });
 
  constructor(private fb: FormBuilder, private user: UserService) {}
 
  get email() {
      return this.form.controls['email'];
  }
 
}

Synchronous and asynchronous validators cannot be mixed, so that is why we have used the more powerful configuration object syntax to bind an asynchronous validator to the email field.

We are passing to the validator creation function userExistsValidator an input argument of type UserService. This is the service that we are going to be using to call the backend and check if a user with the same email already exists.

How to write an Asynchronous Validator

And here is what our userExistsValidator asynchronous validator looks like:

import { AbstractControl, AsyncValidatorFn } from "@angular/forms";
 
export function userExistsValidator(user: UserService): AsyncValidatorFn {
  return (control: AbstractControl) => {
    return user
      .findUserByEmail(control.value)
      .pipe(map((user) => (user ? { userExists: true } : null)));
  };
}

As we can see, this is very similar to a normal validator like we have implemented before. The main difference is that the type returned by the validator creation function userExistsValidator() is of type AsyncValidatorFn.

This means that the validation function itself needs to return either a promise or an Observable that emits a value of type ValidationErrors.

In the case of our implementation, we have used Observables, by calling the HTTP based UserService that called our backend and checked if the user exists on the database.

The last piece of the puzzle for fully understanding custom validators, is that updateOn property that we have used on the email field.

How to use the updateOn field property

The updateOn property can be used both at the level of the form control, like the email field, or at the level of the whole form, like in the case of the start date / end date example.

The updateOn property is useful to define when the form value should be updated with the latest form field value.

When we link a form control such as a text input box to a form model, either by using ngModel, formControl, or formControlName, what happens is that the value of the form control will start to be tracked by the parent form.

When the user starts typing new values or changing the value of the form field, that new value will then be communicated back to the parent form.

The parent form will then need to update its global form.value property, but it will also need to determine if the new value is valid or not.

And for that, it needs to call the validators of the form field and calculate a new field validity state, which will then be used to calculate the new form validity state.

The question is, when will the new value of the form field be communicated back to the parent form?

This is important because by default the value is communicated as quickly as possible. For example, in the case of an input text field, this will by default be communicated with each key pressed by the user, which might be too fast for certain scenarios.

For example, in the case of our userExistsValidator, this would be too much because a new HTTP request to the backend would be triggered with each key pressed.

Instead, we would like the request to be done to the backend only when the user finishes typing the email, so that is why we have set the updateOn property to blur.

This way, only when the user finishes interacting with the email field and moves to another field (either by tabbing away or clicking elsewhere), will then a single HTTP request be triggered to validate the email.

Note that while the asynchronous validation is in progress, the ng-pending css state class will be applied to the email field.

This can be used for styling the email field and informing the user that the validation is in progress if needed.

When to set updateOn to blur?

Besides asynchronous validators, we might also want to set updateOn to blur due to the behavior of certain plain synchronous validators as well.

For example, something as simple as the email validator might not work well if it gets triggered with every key pressed.

When the user starts typing an email, the email will not immediately be valid, which might cause an error message like "The email is invalid" to be shown to the user while he is still typing and then disappear as the email is fully typed, which might be a bit awkward for the user.

What are the different values that updateOn can take?

There are three values that we can set for the updateOn property:

  • change: this is the default value. It means that the form will be updated with every new field value (causing all field validators to be triggered). For a select box or checkbox this will happen when the user chooses a new value, but for a text box this will happen with every key pressed

  • blur: this means that the form will be updated with the new field value only when the field is blurred, meaning that the user either tabbed away or clicked elsewhere on the page

  • submit: this is rarely used, but its available if needed. This means that the form will only be updated with the new value immediately after the form gets submitted.

The updateOn property can be defined both at the level of the form field and also at level of the overall form itself.

Summary

Let's now quickly summarize everything that we have learned about custom form validators:

  • a custom validator is a function that validates the value of a form field according to a business rule, such as form example the field is mandatory or the password needs to have a certain strength

  • With reactive forms, it's much easier to define custom validators. All we have to do is to define a function, and plug it into the form configuration.

  • with template-driven forms, it's also possible to do custom form validation but it's a bit trickier. We will have to define besides the validator function a custom directive and plug it into the dependency injection system

  • Validators can also be defined at the form level, and this is especially useful for writing validators that depend on the values of multiple fields, such as for example to ensure that the password and confirm password fields in a user creation form are identical

  • certain validators can need to perform an asynchronous operation, like calling a backend or a promise-based library. This can be implemented by having the validator return either a promise or an observable

  • For validation to work properly, it's usually necessary to fine-tune exactly when the validators of a field are triggered. This can be done using the updateOn property

I hope that you have enjoyed this post, if you would like to learn a lot more about Angular Forms, we recommend checking the Angular Forms In Depth course, where validators, custom form controls, and many other advanced form topics are covered in detail.

Also, if you have some questions or comments please let me know in the comments below and I will get back to you.

s