Skip to content

Using Custom Async Validators in Angular Reactive Forms

This article was written over 18 months ago and may contain information that is out of date. Some content may be relevant but please refer to the relevant official documentation or available resources for the latest information.

What is a validator in Angular?

First, let's talk about Validators. What is a validator, and how we can use them to improve our forms.

Ocasionally, we want to validate some input in our fields before submission, which will be validated against an asynchronous source. For instance, you may want to check if a label or some data exists before submission. In Angular, you can do this using Async Validators.

Create a basic application

We are going to create a very minimalist form that does one thing: check if the username already exists. In this case, we are going to use a mix of async and sync validators to accomplish our task.

It's very common, in most registration forms, to require an unique username. Using an API call, we to capture if that username is already in use according to the database.

import { Component } from '@angular/core';
import { FormBuilder, Validators } from '@angular/forms';

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
})
export class AppComponent {
  constructor(private fb: FormBuilder) {}

  registrationForm = this.fb.group({
    name: [null, [Validators.minLength(3), Validators.required]],
    username: [null, [Validators.minLength(3), Validators.required]],
  });
}

Created a basic form that has a FormGroup called registrationForm, with 2 FormControl. Also, I added 2 sync validators for make the input required and also to check if the input has a minimum length, using Validators.required and Validators.minLength in each case.

<p>Using Custom Async Validators</p>

<div class="container" [formGroup]="registrationForm">
  <mat-form-field class="example-full-width">
    <mat-label>Name</mat-label>
    <input matInput placeholder="Ex. John Doe" formControlName="name" />
  </mat-form-field>

  <mat-form-field class="example-full-width">
    <mat-label>Username</mat-label>
    <input matInput placeholder="Ex. Batman" formControlName="username" />
  </mat-form-field>

  <div class="button-container">
    <button
      mat-raised-button
      color="primary"
      [disabled]="!registrationForm.valid"
    >
      Submit
    </button>
  </div>
</div>
async validator 1/3

I'm using @angular/material just to simplify our styles, and because it provides a clean way to display errors using the mat-error component. This component will be shown to the user if the required error is present. The button also checks if the FormGroup is valid, otherwise it will be disabled.

Async validators

Our current app works pretty well, and in most cases, will meet all of the requirements. But what if we want to make sure that the username is unique before allowing the user to submit their information?. Creating an Async Validator could be simple. Let's find how to create it, and add it to our current form.

import { Injectable } from '@angular/core';
import { of } from 'rxjs';
import { delay } from 'rxjs/operators';

@Injectable({
  providedIn: 'root',
})
export class UserService {
  private existingUsernames = ['Batman', 'Superman', 'Joker', 'Luthor'];

  checkIfUsernameExists(value: string) {
    return of(this.existingUsernames.some((a) => a === value)).pipe(
      delay(1000)
    );
  }
}

Our method to check if the username already exists is called checkIfUsernameExists. It returns an observable with a 5 seconds delay to simulate a very slow API call. Now, we can create our Async Validator to check if the username exists against that method.

import {
  AbstractControl,
  AsyncValidatorFn,
  ValidationErrors,
} from '@angular/forms';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { UserService } from './user.service';

export class UsernameValidator {
  static createValidator(userService: UserService): AsyncValidatorFn {
    return (control: AbstractControl): Observable<ValidationErrors> => {
      return userService
        .checkIfUsernameExists(control.value)
        .pipe(
          map((result: boolean) =>
            result ? { usernameAlreadyExists: true } : null
          )
        );
    };
  }
}

Our UsernameValidator class takes our UserService as an argument. This method returns a AsyncValidatorFn which receives the FormControl that is placed on, providing us access to the current value.

An AsyncValidatorFn must return either a promise or an Observable of type ValidationErrors.

We use the RxJS map operator to check the value emitted, and either return null if the user doesn't exist, or return a ValidationError with an error type of usernameAlreadyExists.

import { Component } from '@angular/core';
import { FormBuilder, Validators } from '@angular/forms';
import { UserService } from './user.service';
import { UsernameValidator } from './username-validator';

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss'],
})
export class AppComponent {
  constructor(private fb: FormBuilder, private userService: UserService) {}

  registrationForm = this.fb.group({
    name: [null, [Validators.minLength(3), Validators.required]],
    username: [
      null,
      [Validators.minLength(3), Validators.required],
      [UsernameValidator.createValidator(this.userService)],
    ],
  });
}

We import our UsernameValidator and UserService into our component, and declare in the constructor component.

The UsernameValidator is added to our FormControl as the third parameter, calling the createValidator method, and passing a reference to the UserService.

<mat-form-field class="example-full-width">
  <mat-label>Username</mat-label>
  <input matInput placeholder="Ex. Batman" formControlName="username" />
  <mat-error
    *ngIf="registrationForm.get('username').hasError('usernameAlreadyExists')"
  >
    Username already <strong>exists</strong>
  </mat-error>
</mat-form-field>

We update our template to check for an additional error called usernameAlreadyExists, so if our UserService finds that the username already exists, it will provide a to the user based on her status.

The validation status of the control. There are four possible validation status values:

VALID: This control has passed all validation checks.

INVALID: This control has failed at least one validation check.

PENDING: This control is in the midst of conducting a validation check.

DISABLED: This control is exempt from validation checks

async validator 3/3
async validator 2/3

Let's see StackBlitz in action.

Conclusion

Creating beautiful forms for our web app might seem simple, but it's typically more complicated than one would expect. Sometimes, we also need to verify the information, because we don't want to blindly send data to the backend, and then reject it. This can lead to a poor experience for users and result in low adoption rates.

Creating an AsyncValidator can help us improve the user-experience, and also avoid sending data to the backend, resulting in a rejection.

This Dot is a consultancy dedicated to guiding companies through their modernization and digital transformation journeys. Specializing in replatforming, modernizing, and launching new initiatives, we stand out by taking true ownership of your engineering projects.

We love helping teams with projects that have missed their deadlines or helping keep your strategic digital initiatives on course. Check out our case studies and our clients that trust us with their engineering.

Let's innovate together!

We're ready to be your trusted technical partners in your digital innovation journey.

Whether it's modernization or custom software solutions, our team of experts can guide you through best practices and how to build scalable, performant software that lasts.

Prefer email? hi@thisdot.co