This tutorial is out of date and no longer maintained.
Warning: This tutorial was originally written with an earlier versions of Angular and RxJS. This code no longer works as-is.
Angular’s @angular/forms
package supports custom validators for reactive forms. However, there are certain situations where you will want a validator that validates a value with a backend API. For these scenarios, Angular provides a way to define custom async validators.
In this tutorial, you will create an Angular application that uses a custom async validator to check if an email provided by a user does not already exist in the list of users.
This project assumes that you are building from an existing Angular project generated with Angular CLI.
In a real-world scenario, you would call a real backend. For the purposes of this tutorial, you will simulate a backend response as a JSON file containing an array of users and email addresses.
In your code editor, create a new users.json
file and save it to the assets
directory. Add the following example users:
[
{ "name": "Paul", "email": "paul@example.com" },
{ "name": "Ringo", "email": "ringo@example.com" },
{ "name": "John", "email": "john@example.com" },
{ "name": "George", "email": "george@example.com" }
]
At this point, you have a newly created Angular application and an array of users and email addresses.
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:
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 would 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.
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:
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:
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)
]
});
}
In the template things are really as simple as it gets:
<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'">
This email is available.
</div>
<div *ngIf="myForm.get('email').errors && myForm.get('email').errors.emailTaken">
This email is not available.
</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.
Note: 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.
In this tutorial, you created an Angular application that uses a custom async validator to check if an email provided by a user does not already exist in the list of users.
Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.
This textbox defaults to using Markdown to format your answer.
You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!
Sign up for Infrastructure as a Newsletter.
Working on improving health and education, reducing inequality, and spurring economic growth? We'd like to help.
Get paid to write technical tutorials and select a tech-focused charity to receive a matching donation.