A standard validator operates under the assumption that the validity of a certain input can be calculated in a short amount of time that the application can wait to get over with before it continues further. What's more, Angular will run this validation every time the validator is invoked, which might be quite often if form validation is bound to rapid-fire events such as keypresses.
Therefore, it makes good sense that a construct exists that will allow you to smoothly handle the validation procedures that take an arbitrary amount of time to execute or procedures that might not return at all. For this, Angular offers async Validator
, which is fully compatible with Promises
.
The code, links, and a live example related to this recipe are available at .
Suppose you had started with the following skeleton application:
[app/article-editor.component.ts] import {Component} from '@angular/core'; import {FormControl} from '@angular/forms'; @Component({ selector: 'article-editor', template: ` <h2>New York-Style Pizza Actually Saucy Cardboard</h2> <textarea [formControl]="bodyControl" placeholder="Article text"> </textarea> <p><button (click)="saveArticle()">Save</button></p> ` }) export class ArticleEditorComponent { articleBody:string = ''; bodyControl:FormControl = new FormControl(); saveArticle():void { if (this.bodyControl.valid) { alert('Valid!'); } else { alert('Invalid!'); } } }
Your objective is to configure this form in a way that it will become valid only 5 seconds after the user enters the input in order to deter simple spambots.
First, create your validator class, and inside it, place a static validation method. This is similar to a synchronous validation method, but it will instead return a Promise
object, passing the result data to the resolve
method. The FormControl
object accepts the async Validator
as its third argument. If you weren't using any normal Validators
, you could leave it as null
.
As you would combine several Validators
into one using Validators.compose
, async Validators
can be combined using Validators.composeAsync
.
Create the validator skeleton in its own file:
[app/delay.validator.ts] import {FormControl, Validator} from '@angular/forms'; export class DelayValidator implements Validator { static validate(c:FormControl):Promise<{[key:string]:any}> { } }
Though the validator does not yet do anything, you may still add it to the component:
[app/article-editor.component.ts] import {Component} from '@angular/core'; import {FormControl, Validators} from '@angular/forms'; import {DelayValidator} from './delay.validator'; @Component({ selector: 'article-editor', template: ` <h2>New York-Style Pizza Actually Saucy Cardboard</h2> <textarea [formControl]="bodyControl" placeholder="Article text"> </textarea> <p><button (click)="saveArticle()">Save</button></p> ` }) export class ArticleEditorComponent { articleBody:string = ''; bodyControl:FormControl = new FormControl(null, null, DelayValidator.validate); saveArticle():void { if (this.bodyControl.valid) { alert('Valid!'); } else { alert('Invalid!'); } } }
The validator must return a promise, but this promise doesn't ever need to be resolved. Furthermore, you'd like to set the delay to only one time per rendering. So in this case, you can just attach the promise to the FormControl
:
[app/delay.validator.ts] import {FormControl, Validator} from '@angular/forms'; export class DelayValidator implements Validator { static validate(c:FormControl):Promise<{[key:string]:any}> { if (c.pristine && !c.value) { return new Promise; } if (!c.delayPromise) { c.delayPromise = new Promise((resolve) => { setTimeout(() => { console.log('resolve'); resolve(); }, 5000); }); } return c.delayPromise; } }
With this addition, the form will remain invalid until 5 seconds after the first time the value of the textarea
is changed.
Asynchronous validators are handled independently via regular (synchronous) validators, but other than their internal latency differences, they ultimately behave in nearly the exact same way. The important difference is that an async Validator
, apart from the valid and invalid states that it shares with a normal Validator
, has a pending state. The FormControl
will remain in this state until a promise is made indicating the Validator
will return either resolves or rejects.
A FormControl
in a pending
state is treated as invalid for the purpose of checking the validity of aggregating constructs, such as FormGroup
or FormArray
.
In the Validator
you just created, checking the pristine
property of FormControl
is a fine way of ascertaining whether or not the form is "fresh." Before the user modifies the input, pristine
is true
; following any modification (even removing all of the entered text), pristine
becomes false
. Therefore, it is a perfect tool in this example, as it allows us to have the FormControl
maintain the form state without overcomplicating the Validator
.
It's critical to note the form that this validator takes. The validation method inside the DelayValidator
class is a static method and nowhere is the DelayValidator
class being instantiated. The purpose of the class is merely to house the validator. Therefore, you are unable to store information inside this class, since there are no instances in which you can do so.
In this example, you might be tempted to add member data to the validator since you want to track whether the input has been modified yet. Doing so is very much an anti-pattern! The FormControl
object should act as your sole source of stateful information in this scenario. A FormControl
object is instantiated for each input field, and therefore it is the ideal "datastore" with which you can track what the input is doing.
If you were to inspect when the validator method is being called, you would find that it executes only on a keypress inside textarea
. This may seem arbitrary, but the default FormControl
/input
assignment is to evaluate the validators of FormControl
on a change
event emitted from the input. FormControl
objects expose a registerOnChange
method, which lets you hook onto the same point that the validators will be evaluated.