Книга: Angular 2 Cookbook
Назад: Implementing basic forms with FormBuilder and formControlName
Дальше: Creating and using a custom asynchronous validator with Promises

Creating and using a custom validator

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.

Note

The code, links, and a live example related to this recipe are available at .

Getting ready

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.)

How to do it...

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!');       }     }   }   

Note

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.

How it works...

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.

There's more...

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.

Refactoring into validator attributes

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.

Note

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.

Tip

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.

See also

  • Creating and using a custom asynchronous validator with Promises shows how Angular allows you to have a delayed evaluation of the form state
Назад: Implementing basic forms with FormBuilder and formControlName
Дальше: Creating and using a custom asynchronous validator with Promises

thank you
Flame
cant read the code since it is all on a single line. Also this comments section is russian
Rakuneque
DATA COLLECTION AND ANALYSIS Two reviewers extracted data and assessed methodological quality independently lasix torsemide conversion Many others were in that space already