The basic built-in validators that Angular provides will get you off the ground, but if your application relies on forms, you will undoubtedly come to a point where you will want to define your own validator logic.
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, Validators} from '@angular/forms'; @Component({ selector: 'article-editor', template: ` <h2>Psych Study on Humility Wins Major Award</h2> <textarea [formControl]="bodyControl" placeholder="Article text"></textarea> <p><button (click)="saveArticle()">Save</button></p> ` }) export class ArticleEditorComponent { articleBody:string = ''; bodyControl:Control = new FormControl(null, Validators.required); saveArticle():void { if (this.bodyControl.valid) { alert('Valid!'); } else { alert('Invalid!'); } } }
Your objective is to add an additional validation to the textarea
that will limit it to 10 words. (The editorial staff is big on brevity.)
If you look at the function signature of an AbstractControl
, you will notice that the validator argument is just a ValidatorFn
. This validator function can be any function that accepts an AbstractControl
object as its sole argument and returns an object keyed with strings
for the error object. This error object acts as a dictionary of errors, and a validator can return as many errors as applicable. The value of the dictionary entry can (and should) contain metadata about what is causing the error. If there are no errors found by the custom validator, it should just return null
.
The simplest way to implement this is by adding a member method to the component:
[app/article-editor.component.ts] export class ArticleEditor { articleBody:string bodyCtrl:Control constructor() { this.articleBody = ''; this.bodyCtrl = new Control('', Validators.required); } wordCtValidator(c:Control): {[key: string]: any} { let wordCt:number = (c.value.match(/\S+/g) || []).length; return wordCt <= 10 ? null : { 'maxwords': { 'limit':10, 'actual':wordCt } }; } saveArticle() { if (this.bodyCtrl.valid) { alert('Valid!'); } else { alert('Invalid!'); } } }
Here, you're using a regular expression to match any non-whitespace strings, which can be treated as a "word." You also need to initialize the FormControl
object to an empty string since you are using the string
prototype's match
method. Since this regular expression will return null when there are no matches, a fallback || []
clause is added to always yield something that has a length
method.
Now that the validator method is defined, you need to actually use it on FormControl
. Angular allows you to bundle an array of validators into a single validator, evaluating them in order:
[app/article-editor.component.ts] import {Component} from '@angular/core'; import {FormControl, Validators} from '@angular/forms'; @Component({ selector: 'article-editor', template: ` <h2>Psych Study on Humility Wins Major Award</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, [Validators.required, this.wordCtValidator]); wordCtValidator(c:FormControl):{[key: string]: any} { let wordCt:number = ((c.value || '').match(/\S+/g) || []).length; return wordCt <= 10 ? null : {maxwords: {limit:10, actual:wordCt}}; } saveArticle():void { if (this.bodyControl.valid) { alert('Valid!'); } else { alert('Invalid!'); } } }
With this, your FormControl
should now only be valid when there are 10 words or fewer and the input is not empty.
A FormControl
expects a ValidatorFn
with a specified return type, but it does not care where it comes from. Therefore, you were able to define a method inside the component class and just pass it along when FormControl
was instantiated.
The FormControl
object associated with a given input must be able to have validators associated with it. In this recipe, you first implemented custom validation using explicit association via the instantiation arguments and defining the validator as a simple standalone ValidationFn
.
Your inner software engineer should be totally dissatisfied with this solution. The validator you just defined cannot be used outside this component without injecting the entire component, and explicitly listing every validator when instantiating the FormControl
is a major pain.
A superior solution is to implement a formal Validator
class. This has several benefits: you will be able to import/export the class and use the validator as an attribute in the template, which obviates the need for bundling validators with Validators.compose
.
Your strategy should be to create a directive that can function not only as an attribute, but also as something that Angular can recognize as a formal Validator
and automatically incorporate it as such. This can be accomplished by creating a directive that implements the Validator
interface and also bundles the new Validator
directive into the existing NG_VALIDATORS
token.
For now, don't worry about the specifics of what is happening with the providers
array inside the directive metadata object. This will be covered in depth in the chapter on dependency injection. All that you need to know here is that this code is allowing the FormControl
object bound to textarea
to associate the custom validator you are building with it.
First, move the validation method to its own directive by performing the steps mentioned in the preceding paragraph:
[app/max-word-count.validator.ts] import {Directive} from '@angular/core'; import {Validator, FormControl, NG_VALIDATORS} from '@angular/forms'; @Directive({ selector: '[max-word-count]', providers: [{ provide:NG_VALIDATORS, useExisting: MaxWordCountValidator, multi: true }] }) export class MaxWordCountValidator implements Validator { validate(c:FormControl):{[key:string]:any} { let wordCt:number = ((c.value || '') .match(/\S+/g) || []).length; return wordCt <= 10 ? null : {maxwords: {limit:10, actual:wordCt}}; } }
Next, add this directive to the application module:
[app/app.module.ts] import {NgModule} from '@angular/core'; import {BrowserModule} from '@angular/platform-browser'; import {ReactiveFormsModule} from '@angular/forms'; import {ArticleEditorComponent} from './article-editor.component'; import {MaxWordCountValidator} from './max-word-count.validator'; @NgModule({ imports: [ BrowserModule, ReactiveFormsModule ], declarations: [ ArticleEditorComponent, MaxWordCountValidator ], bootstrap: [ ArticleEditorComponent ] }) export class AppModule {}
This makes it available to all the components in this module. What's more, the provider configuration you specified before allows you to simply add the directive attribute to any input, and Angular will be able to incorporate its validation function into that FormControl
. The integration is as follows:
[app/article-editor.component.ts] import {Component} from '@angular/core'; import {FormControl} from '@angular/forms'; @Component({ selector: 'article-editor', template: ` <h2>Psych Study on Humility Wins Major Award</h2> <textarea [formControl]="bodyControl" required max-word-count 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!'); } } }
This is already far superior. The MaxWordCount
directive can now be imported and used anywhere in our application by simply listing it as a directive dependency in a component. There's no need for the Validator.compose
nastiness when instantiating a FormControl
object.
This is especially useful when you are implicitly creating these FormControl
objects with formControl
and other built-in form directives, which for many applications will be the primary form utilization method. Building your custom validator as an attribute directive will integrate seamlessly in these situations.
You should still be dissatisfied though, as the validator is hardcoded to check for 10 words. You would instead like to leave this up to the input that is using it. Therefore, you should change the directive to accept a single parameter, which will take the form of the attribute's value:
[app/max-word-count.validator.ts] import {Directive} from '@angular/core'; import {Validator, FormControl, NG_VALIDATORS} from '@angular/forms'; @Directive({ selector: '[max-word-count]', inputs: ['rawCount: max-word-count'], providers: [{ provide:NG_VALIDATORS, useExisting: MaxWordCountValidator, multi: true }] }) export class MaxWordCountValidator implements Validator { rawCount:string; validate(c:FormControl):{[key:string]:any} { let wordCt:number = ((c.value || '').match(/\S+/g) || []).length; return wordCt <= this.maxCount ? null : {maxwords: {limit:this.maxCount, actual:wordCt}}; } get maxCount():number { return parseInt(this.rawCount); } } [app/article-editor.component.ts] import {Component} from '@angular/core'; import {FormControl} from '@angular/forms'; @Component({ selector: 'article-editor', template: ` <h2>Psych Study on Humility Wins Major Award</h2> <textarea [formControl]="bodyControl" required max-word-count="10" 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!'); } } }
Now you have defined the value of the attribute as an input to the validator, which you can then use to configure how the validator will operate.