表單是幾乎每個(gè) Web 應(yīng)用程序的一部分。雖然 Angular 為我們提供了幾個(gè)內(nèi)置 validators (驗(yàn)證器),但在實(shí)際工作中為了滿足項(xiàng)目需求,我們經(jīng)常需要為應(yīng)用添加一些自定義驗(yàn)證功能。接下來我們將著重介紹,如何自定義 validator 指令。
Built-in Validators
Angular 提供了一些內(nèi)置的 validators,我們可以在 Template-Driven 或 Reactive 表單中使用它們。如果你對(duì) Template-Driven 和 Reactive 表單還不了解的話,可以參考 Angular 4 Forms 系列中 Template Driven Forms 和 Reactive Forms 這兩篇文章。
在寫本文時(shí),Angular 支持的內(nèi)建 validators 如下:
- required - 設(shè)置表單控件值是非空的
- email - 設(shè)置表單控件值的格式是 email
- minlength - 設(shè)置表單控件值的最小長(zhǎng)度
- maxlength - 設(shè)置表單控件值的最大長(zhǎng)度
- pattern - 設(shè)置表單控件的值需匹配 pattern 對(duì)應(yīng)的模式
在使用內(nèi)建 validators 之前,我們需要根據(jù)使用的表單類型 (Template-Driven 或 Reactive),導(dǎo)入相應(yīng)的模塊,對(duì)于 Template-Driven
表單,我們需要導(dǎo)入 FormsModule
。具體示例如下:
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 {}
一旦導(dǎo)入了 FormsModule
模塊,我們就可以在應(yīng)用中使用該模塊提供的所有指令:
<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>
而對(duì)于 Reactive
表單,我們就需要導(dǎo)入 ReactiveFormsModule
模塊:
import { ReactiveFormsModule } from '@angular/forms';
@NgModule({
imports: [BrowserModule, ReactiveFormsModule],
...
})
export class AppModule {}
可以直接使用 FormControl
和 FormGroup
API 創(chuàng)建表單:
@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,采用更便捷的方式創(chuàng)建表單:
@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 中的表單對(duì)象關(guān)聯(lián)起來,具體如下:
<form novalidate [formGroup]="form">
...
</form>
接下來我們來介紹一下如何自定義 validator 指令。
Building a custom validator directive
在實(shí)際開發(fā)前,我們先來介紹一下具體需求:我們有一個(gè)新增用戶的表單頁(yè)面,里面包含 4 個(gè)輸入框,分為用于保存用戶輸入的 username
、email
、password
、confirmPassword
信息。具體的 UI 效果圖如下:
Setup (基礎(chǔ)設(shè)置)
1.定義 user 接口
export interface User {
username: string; // 必填,5-8個(gè)字符
email: string; // 必填,有效的email格式
password: string; // 必填,值要與confirmPassword值一樣
confirmPassword: string; // 必填,值要與password值一樣
}
2.導(dǎo)入 ReactiveFormsModule
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 { }
3.初始化 AppComponent
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個(gè)字符
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 {
}
}
Custom confirm password validator
接下來我們來實(shí)現(xiàn)自定義 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; // 獲取應(yīng)用該指令,控件上的值
// control value (e.g. password)
let e = c.root.get(this.validateEqual); // 獲取進(jìn)行值比對(duì)的控件
// value not equal
if (e && v !== e.value)
return {
validateEqual: false
}
return null;
}
}
上面的代碼很長(zhǎng),我們來分解一下。
Directive declaration
@Directive({
selector: '[validateEqual][formControlName],[validateEqual]
[formControl],[validateEqual][ngModel]',
providers: [
{ provide: NG_VALIDATORS, useExisting: forwardRef(() => EqualValidator),
multi: true }
]
})
首先,我們使用 @Directive
裝飾器來定義指令。然后我們?cè)O(shè)置該指令的 Metadata 信息:
- selector - 定義指令在 HTML 代碼中匹配的方式
- providers - 注冊(cè)EqualValidator
其中 forwardRef 的作用,請(qǐng)參考 - Angular 4 Forward Reference
Class defintion
export class EqualValidator implements Validator {
constructor(@Attribute('validateEqual') public validateEqual: string) {}
validate(c: AbstractControl): { [key: string]: any } {}
}
我們的 EqualValidator
類必須實(shí)現(xiàn) Validator
接口:
export interface Validator {
validate(c: AbstractControl): ValidationErrors|null;
registerOnValidatorChange?(fn: () => void): void;
}
該接口要求定義一個(gè) validate()
方法,因此我們的 ``EqualValidator類中就需要實(shí)現(xiàn)
Validator接口中定義的
validate方法。此外在構(gòu)造函數(shù)中,我們通過
@Attribute('validateEqual')` 裝飾器來獲取 validateEqual 屬性上設(shè)置的值。
Validate implementation
validate(c: AbstractControl): { [key: string]: any } {
// self value (e.g. retype password)
let v = c.value; // 獲取應(yīng)用該指令,控件上的值
// control value (e.g. password)
let e = c.root.get(this.validateEqual); // 獲取進(jìn)行值比對(duì)的控件
// value not equal
if (e && v !== e.value)
return { // 若不相等,返回驗(yàn)證失敗信息
validateEqual: false
}
return null;
}
Use custom validator
要在我們的表單中使用自定義驗(yàn)證器,我們需要將其導(dǎo)入到我們的應(yīng)用程序模塊中。
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 { }
以上代碼成功運(yùn)行后,我們來驗(yàn)證一下剛實(shí)現(xiàn)的功能:
友情提示:演示需要先把密碼框的類型設(shè)置為text
- 步驟一
- 步驟二
看起來一切很順利,但請(qǐng)繼續(xù)看下圖:
什么情況,password 輸入框的值已經(jīng)變成 12345 了,還能驗(yàn)證通過。為什么會(huì)出現(xiàn)這個(gè)問題呢?因?yàn)槲覀兊闹辉?confirmPassword 輸入框中應(yīng)用 validateEqual
指令。所以 password 輸入框的值發(fā)生變化時(shí),是不會(huì)觸發(fā)驗(yàn)證的。接下來我們來看一下如何修復(fù)這個(gè)問題。
Solution
我們將重用我們的 validateEqual 驗(yàn)證器并添加一個(gè) 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>
- 若未設(shè)置
reverse
屬性或?qū)傩灾禐?false,實(shí)現(xiàn)的功能跟前面的一樣。 - 若
reverse
的值設(shè)置為 true,我們?nèi)匀粫?huì)執(zhí)行相同的驗(yàn)證,但錯(cuò)誤信息不是添加到當(dāng)前控件,而是添加到目標(biāo)控件上。
在上面的示例中,我們?cè)O(shè)置 password 輸入框的 reverse 屬性為 true,即 reverse="true"
。當(dāng) password 輸入框的值與 confirmPassword 輸入框的值不相等時(shí),我們將把錯(cuò)誤信息添加到 confirmPassword 控件上。具體實(shí)現(xiàn)如下:
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
// 未設(shè)置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,則把異常信息添加到比對(duì)的目標(biāo)控件上
if (e && v !== e.value && this.isReverse) {
e.setErrors({ validateEqual: false });
}
return null;
}
}
以上代碼運(yùn)行后,成功解決了我們的問題。其實(shí)解決該問題還有其它的方案,我們可以基于 password
和 confirmPassword
來創(chuàng)建 FormGroup
對(duì)象,然后添加自定義驗(yàn)證來實(shí)現(xiàn)上述的功能。詳細(xì)的信息,請(qǐng)參考 - Angular 4 基于AbstractControl自定義表單驗(yàn)證。