表單是幾乎每一個 Web 應用程序的一部分。雖然 Angular 爲咱們提供了幾個內置 validators (驗證器),但在實際工做中爲了知足項目需求,咱們常常須要爲應用添加一些自定義驗證功能。接下來咱們將着重介紹,如何自定義 validator 指令。html
Angular 提供了一些內置的 validators,咱們能夠在 Template-Driven 或 Reactive 表單中使用它們。若是你對 Template-Driven 和 Reactive 表單還不瞭解的話,能夠參考 Angular 4.x Forms 系列中 Template Driven Forms 和 Reactive Forms 這兩篇文章。typescript
在寫本文時,Angular 支持的內建 validators 以下:json
required - 設置表單控件值是非空的bootstrap
email - 設置表單控件值的格式是 emailsegmentfault
minlength - 設置表單控件值的最小長度app
maxlength - 設置表單控件值的最大長度ide
pattern - 設置表單控件的值需匹配 pattern 對應的模式函數
在使用內建 validators 以前,咱們須要根據使用的表單類型 (Template-Driven 或 Reactive),導入相應的模塊,對於 Template-Driven
表單,咱們須要導入 FormsModule
。具體示例以下:ui
import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { FormsModule } from '@angular/forms'; @NgModule({ imports: [BrowserModule, FormsModule], // we add FormsModule here declarations: [AppComponent], bootstrap: [AppComponent] }) export class AppModule {}
一旦導入了 FormsModule
模塊,咱們就能夠在應用中使用該模塊提供的全部指令:this
<form novalidate> <input type="text" name="name" ngModel required> <input type="text" name="street" ngModel minlength="3"> <input type="text" name="city" ngModel maxlength="10"> <input type="text" name="zip" ngModel pattern="[A-Za-z]{5}"> </form>
而對於 Reactive
表單,咱們就須要導入 ReactiveFormsModule
模塊:
import { ReactiveFormsModule } from '@angular/forms'; @NgModule({ imports: [BrowserModule, ReactiveFormsModule], ... }) export class AppModule {}
能夠直接使用 FormControl
和 FormGroup
API 建立表單:
@Component() class Cmp { form: FormGroup; ngOnInit() { this.form = new FormGroup({ name: new FormControl('', Validators.required)), street: new FormControl('', Validators.minLength(3)), city: new FormControl('', Validators.maxLength(10)), zip: new FormControl('', Validators.pattern('[A-Za-z]{5}')) }); } }
也能夠利用 FormBuilder
提供的 API,採用更便捷的方式建立表單:
@Component() class Cmp { constructor(private fb: FormBuilder) {} ngOnInit() { this.form = this.fb.group({ name: ['', Validators.required], street: ['', Validators.minLength(3)], city: ['', Validators.maxLength(10)], zip: ['', Validators.pattern('[A-Za-z]{5}')] }); } }
須要注意的是,咱們還須要使用 [formGroup]
指令將表單模型與 DOM 中的表單對象關聯起來,具體以下:
<form novalidate [formGroup]="form"> ... </form>
接下來咱們來介紹一下如何自定義 validator 指令。
在實際開發前,咱們先來介紹一下具體需求:咱們有一個新增用戶的表單頁面,裏面包含 4 個輸入框,分爲用於保存用戶輸入的 username
、email
、password
、confirmPassword
信息。具體的 UI 效果圖以下:
export interface User { username: string; // 必填,5-8個字符 email: string; // 必填,有效的email格式 password: string; // 必填,值要與confirmPassword值同樣 confirmPassword: string; // 必填,值要與password值同樣 }
app.module.ts
import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { ReactiveFormsModule } from '@angular/forms'; import { AppComponent } from './app.component'; @NgModule({ imports: [BrowserModule, ReactiveFormsModule], declarations: [AppComponent], bootstrap: [AppComponent] }) export class AppModule { }
app.component.html
<div> <h3>Add User</h3> <form novalidate (ngSubmit)="saveUser()" [formGroup]="user"> <div> <label for="">Username</label> <input type="text" formControlName="username"> <div class="error" *ngIf="user.get('username').invalid && user.get('username').touched"> Username is required (minimum 5 characters, maximum 8 characters). </div> <!--<pre *ngIf="user.get('username').errors" class="margin-20"> {{ user.get('username').errors | json }}</pre>--> </div> <div> <label for="">Email</label> <input type="email" formControlName="email"> <div class="error" *ngIf="user.get('email').invalid && user.get('email').touched"> Email is required and format should be <i>24065****@qq.com</i>. </div> <!--<pre *ngIf="user.get('email').errors" class="margin-20"> {{ user.get('email').errors | json }}</pre>--> </div> <div> <label for="">Password</label> <input type="password" formControlName="password"> <div class="error" *ngIf="user.get('password').invalid && user.get('password').touched"> Password is required </div> <!--<pre *ngIf="user.get('password').errors" class="margin-20"> {{ user.get('password').errors | json }}</pre>--> </div> <div> <label for="">Retype password</label> <input type="password" formControlName="confirmPassword" validateEqual="password"> <div class="error" *ngIf="user.get('confirmPassword').invalid && user.get('confirmPassword').touched"> Password mismatch </div> <!--<pre *ngIf="user.get('confirmPassword').errors" class="margin-20"> {{ user.get('confirmPassword').errors | json }}</pre>--> </div> <button type="submit" class="btn-default" [disabled]="user.invalid">Submit</button> </form> </div>
app.component.ts
import { Component, OnInit } from '@angular/core'; import { FormGroup, FormBuilder, Validators } from '@angular/forms'; export interface User { username: string; // 必填,5-8個字符 email: string; // 必填,有效的email格式 password: string; // 必填,值要與confirmPassword值同樣 confirmPassword: string; // 必填,值要與password值同樣 } @Component({ moduleId: module.id, selector: 'exe-app', templateUrl: 'app.component.html', styles: [` .error { border: 1px dashed red; color: red; padding: 4px; } .btn-default { border: 1px solid; background-color: #3845e2; color: #fff; } .btn-default:disabled { background-color: #aaa; } `] }) export class AppComponent implements OnInit { public user: FormGroup; constructor(public fb: FormBuilder) { } ngOnInit() { this.user = this.fb.group({ username: ['', [Validators.required, Validators.minLength(5), Validators.maxLength(8)]], email: ['', [Validators.required, Validators.email]], password: ['', [Validators.required]], confirmPassword: ['', [Validators.required]] }); } saveUser(): void { } }
接下來咱們來實現自定義 equal-validator
指令:
equal-validator.directive.ts
import { Directive, forwardRef, Attribute } from '@angular/core'; import { Validator, AbstractControl, NG_VALIDATORS } from '@angular/forms'; @Directive({ selector: '[validateEqual][formControlName],[validateEqual][formControl], [validateEqual][ngModel]', providers: [ { provide: NG_VALIDATORS, useExisting: forwardRef(() => EqualValidator), multi: true } ] }) export class EqualValidator implements Validator { constructor(@Attribute('validateEqual') public validateEqual: string) { } validate(c: AbstractControl): { [key: string]: any } { // self value (e.g. retype password) let v = c.value; // 獲取應用該指令,控件上的值 // control value (e.g. password) let e = c.root.get(this.validateEqual); // 獲取進行值比對的控件 // value not equal if (e && v !== e.value) return { validateEqual: false } return null; } }
上面的代碼很長,咱們來分解一下。
@Directive({ selector: '[validateEqual][formControlName],[validateEqual] [formControl],[validateEqual][ngModel]', providers: [ { provide: NG_VALIDATORS, useExisting: forwardRef(() => EqualValidator), multi: true } ] })
首先,咱們使用 @Directive
裝飾器來定義指令。而後咱們設置該指令的 Metadata 信息:
selector - 定義指令在 HTML 代碼中匹配的方式
providers - 註冊EqualValidator
其中 forwardRef 的做用,請參考 - Angular 2 Forward Reference
export class EqualValidator implements Validator { constructor(@Attribute('validateEqual') public validateEqual: string) {} validate(c: AbstractControl): { [key: string]: any } {} }
咱們的 EqualValidator
類必須實現 Validator
接口:
export interface Validator { validate(c: AbstractControl): ValidationErrors|null; registerOnValidatorChange?(fn: () => void): void; }
該接口要求定義一個 validate()
方法,所以咱們的 `EqualValidator
類中就須要實現 Validator
接口中定義的 validate
方法。此外在構造函數中,咱們經過 @Attribute('validateEqual')
裝飾器來獲取 validateEqual 屬性上設置的值。
validate(c: AbstractControl): { [key: string]: any } { // self value (e.g. retype password) let v = c.value; // 獲取應用該指令,控件上的值 // control value (e.g. password) let e = c.root.get(this.validateEqual); // 獲取進行值比對的控件 // value not equal if (e && v !== e.value) return { // 若不相等,返回驗證失敗信息 validateEqual: false } return null; }
要在咱們的表單中使用自定義驗證器,咱們須要將其導入到咱們的應用程序模塊中。
import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { ReactiveFormsModule } from '@angular/forms'; import { EqualValidator } from './equal-validator.directive'; import { AppComponent } from './app.component'; @NgModule({ imports: [BrowserModule, ReactiveFormsModule], declarations: [AppComponent, EqualValidator], bootstrap: [AppComponent] }) export class AppModule { }
以上代碼成功運行後,咱們來驗證一下剛實現的功能:
友情提示:演示須要先把密碼框的類型設置爲text
步驟一
步驟二
看起來一切很順利,但請繼續看下圖:
什麼狀況,password 輸入框的值已經變成 12345 了,還能驗證經過。爲何會出現這個問題呢?由於咱們的只在 confirmPassword 輸入框中應用 validateEqual
指令。因此 password 輸入框的值發生變化時,是不會觸發驗證的。接下來咱們來看一下如何修復這個問題。
咱們將重用咱們的 validateEqual 驗證器並添加一個 reverse
屬性 。
<div> <label for="">Password</label> <input type="text" formControlName="password" validateEqual="confirmPassword" reverse="true"> <div class="error" *ngIf="user.get('password').invalid && user.get('password').touched"> Password is required </div> <!--<pre *ngIf="user.get('password').errors" class="margin-20"> {{ user.get('password').errors | json }}</pre>--> </div> <div> <label for="">Retype password</label> <input type="text" formControlName="confirmPassword" validateEqual="password"> <div class="error" *ngIf="user.get('confirmPassword').invalid && user.get('confirmPassword').touched"> Password mismatch </div> <!--<pre *ngIf="user.get('confirmPassword').errors" class="margin-20"> {{ user.get('confirmPassword').errors | json }}</pre>--> </div>
若未設置 reverse
屬性或屬性值爲 false,實現的功能跟前面的同樣。
若 reverse
的值設置爲 true,咱們仍然會執行相同的驗證,但錯誤信息不是添加到當前控件,而是添加到目標控件上。
在上面的示例中,咱們設置 password 輸入框的 reverse 屬性爲 true,即 reverse="true"
。當 password 輸入框的值與 confirmPassword 輸入框的值不相等時,咱們將把錯誤信息添加到 confirmPassword 控件上。具體實現以下:
equal-validator.directive.ts
import { Directive, forwardRef, Attribute } from '@angular/core'; import { Validator, AbstractControl, NG_VALIDATORS } from '@angular/forms'; @Directive({ selector: '[validateEqual][formControlName],[validateEqual][formControl], [validateEqual][ngModel]', providers: [ { provide: NG_VALIDATORS, useExisting: forwardRef(() => EqualValidator), multi: true } ] }) export class EqualValidator implements Validator { constructor(@Attribute('validateEqual') public validateEqual: string, @Attribute('reverse') public reverse: string) { } private get isReverse() { if (!this.reverse) return false; return this.reverse === 'true'; } validate(c: AbstractControl): { [key: string]: any } { // self value let v = c.value; // control vlaue let e = c.root.get(this.validateEqual); // value not equal // 未設置reverse的值或值爲false if (e && v !== e.value && !this.isReverse) { return { validateEqual: false } } // value equal and reverse // 若值相等且reverse的值爲true,則刪除validateEqual異常信息 if (e && v === e.value && this.isReverse) { delete e.errors['validateEqual']; if (!Object.keys(e.errors).length) e.setErrors(null); } // value not equal and reverse // 若值不相等且reverse的值爲true,則把異常信息添加到比對的目標控件上 if (e && v !== e.value && this.isReverse) { e.setErrors({ validateEqual: false }); } return null; } }
以上代碼運行後,成功解決了咱們的問題。其實解決該問題還有其它的方案,咱們能夠基於 password
和 confirmPassword
來建立 FormGroup
對象,而後添加自定義驗證來實現上述的功能。詳細的信息,請參考 - Angular 4.x 基於AbstractControl自定義表單驗證。