Custom Async Validators in Angular

We covered how to create custom validators for reactive forms in Angular. Sometimes however, you’ll want a validator that actually validates a value with a backend API. For this, Angular provides an easy way to define custom async validators.

This post covers creating custom async validators for Angular 2+ apps.

Let’s create a simple app that checks against a list of users to ensure that an email is not taken.

Normally you’d call a real backend, but here we’ll create a dummy JSON file that we’ll be able to call from a service using the Http service. If you’re using the Angular CLI, you can put the JSON file in the /assets folder and it’ll be available automatically:

/assets/users.json

[
  { "name": "Paul", "email": "paul@example.com" },
  { "name": "Ringo", "email": "ringo@example.com" },
  { "name": "John", "email": "john@example.com" },
  { "name": "George", "email": "george@example.com" }
]

Signup Service

Next, let’s create a service that has a checkEmailNotTaken method that triggers an http GET call to our JSON file. Here we’re using RxJS’s delay operator to simulate some latency:

signup.service.ts

import { Injectable } from '@angular/core';
import { Http } from '@angular/http';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/filter';
import 'rxjs/add/operator/delay';

@Injectable()
export class SignupService {
  constructor(private http: Http) {}

  checkEmailNotTaken(email: string) {
    return this.http
      .get('assets/users.json')
      .delay(1000)
      .map(res => res.json())
      .map(users => users.filter(user => user.email === email))
      .map(users => !users.length);
  }
}

Notice how we filter for users that have the same email as the one provided to the method. We then map over the results again and test to make sure that we’re getting an empty object.

In a real-world scenario, you'd probably also want to use a combination of the debounceTime and distinctUntilChanged operators as discussed in our post about creating a real time search. Introducing some debouncing like that would help keep the amount of requests sent to your backend API to a minimum.

Component and Async Validator

Our simple component initializes our reactive form and defines our async validator: validateEmailNotTaken. Notice how form controls in our FormBuilder.group declaration can take async validators as a third argument. Here we’re using only one async validator, but you’d want to wrap multiple async validators in an array:

app.component.ts

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

import { SignupService } from './signup.service';

@Component({ ... })
export class AppComponent implements OnInit {
  myForm: FormGroup;

  constructor(
    private fb: FormBuilder,
    private signupService: SignupService
  ) {}

  ngOnInit() {
    this.myForm = this.fb.group({
      name: ['', Validators.required],
      email: [
        '',
        [Validators.required, Validators.email],
        this.validateEmailNotTaken.bind(this)
      ]
    });
  }

  validateEmailNotTaken(control: AbstractControl) {
    return this.signupService.checkEmailNotTaken(control.value).map(res => {
      return res ? null : { emailTaken: true };
    });
  }
}

Our validator is very similar to a typical custom validator. Here we’ve defined our validator in the component class directly instead of a separate file. This makes it easier to access our injected service instance. Notice also how we need bind the this value to insure that it points to the component class.


We could also define our async validator in its own file, for easier reuse and separation of concerns. The only tricky part is to find a way to provide our service instance. Here, for example, we create a class that has a createValidator static method that takes-in our service instance and that returns our validator function:

/validators/async-email.validator.ts

import { AbstractControl } from '@angular/forms';
import { SignupService } from '../signup.service';

export class ValidateEmailNotTaken {
  static createValidator(signupService: SignupService) {
    return (control: AbstractControl) => {
      return signupService.checkEmailNotTaken(control.value).map(res => {
        return res ? null : { emailTaken: true };
      });
    };
  }
}

Then, back in our component, we import our ValidateEmailNotTaken class and we can use our validator like this instead:

ngOnInit() {
  this.myForm = this.fb.group({
    name: ['', Validators.required],
    email: [
      '',
      [Validators.required, Validators.email],
      ValidateEmailNotTaken.createValidator(this.signupService)
    ]
  });
}

Template

In the template things are really as simple as it gets:

app.component.html

<form [formGroup]="myForm">
  <input type="text" formControlName="name">
  <input type="email" formControlName="email">

  <div *ngIf="myForm.get('email').status === 'PENDING'">
    Checking...
  </div>
  <div *ngIf="myForm.get('email').status === 'VALID'">
    😺 Email is available!
  </div>

  <div *ngIf="myForm.get('email').errors && myForm.get('email').errors.emailTaken">
    😢 Oh noes, this email is already taken!
  </div>
</form>

You can see that we display different messages depending on the value of the status property on the email form control. The possible values for status are VALID, INVALID, PENDING and DISABLED. We also display an error message if the async validation errors-out with our emailTaken error.

Form fields that are being validated with an async validator will also have an ng-pending class while validation is pending. This makes it easy to style fields that are currently pending validation.

✨ And there you have it! An easy way to check for validity with a backend API.

✖ Clear

🕵 Search Results

🔎 Searching...