引言
模板驅動表單相比較響應式表單可以少更少的代碼做同樣的事情靶端,可也損失了自由度與更易測試,當然很多人并不在乎啦显歧。
所以我相信很多人在編寫Angular不自由自主去更傾向于模板驅動表單的寫法占遥。
表單最核心的是校驗體驗喊儡,在Angular中簡直就是發(fā)揮到了極致颊糜,比如:required
、min
秃踩、max
衬鱼、pattern
等,這些原本是HTML DOM元素中的表述憔杨,而Angular默認實現(xiàn)了一整套的校驗指令鸟赫,比如:required
對應 RequiredValidator。
然后很多時候我們需要一些特殊的校驗消别,比如:數(shù)據(jù)比較抛蚤、遠程校驗等。那在模板驅動表單風格中我們要如何優(yōu)雅的實現(xiàn)這樣一個校驗器呢寻狂?
一岁经、Angular是如何校驗?
一般在編寫一個手機文本框可能是這樣:
<input [(ngModel)]="user.mobile" #mobile="ngModel" autocomplete="off" type="tel" class="form-control" name="mobile" required maxlength="11">
<div *ngIf="mobile.errors">
<p *ngIf="mobile.errors.required">手機號必填</p>
<p *ngIf="mobile.errors.pattern">手機號格式不正確</p>
</div>
以上幾行很友好的實現(xiàn)從必填項荆虱、格式進行校驗蒿偎,而這一切都是依靠 [(ngModel)]
統(tǒng)一采集,得以只需要利用一個模板引用變量訪問到每個校驗指令的錯誤信息怀读。
1诉位、[(ngModel)]
到底做了什么?
在解析這個問題前需要先了解一下 RequiredValidator 是如何定義的菜枷。
@Directive({
providers: [{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => RequiredValidator),
multi: true
}]
})
export class RequiredValidator {}
只看最核心向 NG_VALIDATORS
標識符注冊一個 RequiredValidator
指令苍糠。這樣就可以使 ngModel
指令中注入 NG_VALIDATORS
后就能得到這個指令對象。
ngModel
我把它簡化了一下:
export class NgModel extends NgControl {
constructor(@Inject(NG_VALIDATORS) validators: Array<Validator|ValidatorFn>) {}
get validator(): ValidatorFn|null {
// 各種校驗并返回結果
}
}
有關更多ng_model.ts可以深入閱讀源代碼啤誊。
Angular會在每一次表單值變更時岳瞭,對所有的表單中已經(jīng)安裝的校驗器進行一次遍歷。
二蚊锹、編寫一個校驗器
誠如 required
校驗器一樣瞳筏,依然是把自定義校驗器掛到 NG_VALIDATORS
當中。假如我們希望手機文本框只能輸入 159
開頭的一個校驗器牡昆。
定義Directive
@Directive({
selector: '[user-mobile]',
exportAs: 'userMobile',
providers: [{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => UserMobileDirective),
multi: true
}]
})
export class UserMobileDirective {}
一個非常普通的指令定義方法姚炕,只是多了一個將 UserMobileDirective
注冊到 NG_VALIDATORS
標識符當中而已。別問我為什么丢烘,一種約定柱宦。
類
export class UserMobileDirective implements Validator {
validate(c: AbstractControl): { [key: string]: any; } {
let value: string = c.value || '';
if (!value.startsWith('159')) {
return {
mobile: {
msg: '手機號必須是159開頭',
actualValue: value
}
};
}
return null;
}
}
只需要實現(xiàn) Validator
接口的 validate
方法即可。
從 c
中獲取DOM值播瞳,當遇到非 159
開頭時掸刊,返回一個用于表述消息的對象即可,否則返回一個 null
赢乓。這個對象會被統(tǒng)一采集在 ngModel.errors
對象下面忧侧。故而石窑,只需要在DOM元素加上 user-mobile
指令即可。
<input user-mobile [(ngModel)]="user.mobile" #mobile="ngModel" autocomplete="off" type="tel" class="form-control" name="mobile" id="mobile" required maxlength="11">
<div *ngIf="mobile.errors">
<p *ngIf="mobile.errors.required">手機號必填</p>
<p *ngIf="mobile.errors.mobile">{{mobile.errors.mobile.msg}}</p>
</div>
接口還包括一個
registerOnValidatorChange
可選方法苍柏,當某些其它外部屬性的變更時尼斧,允許重新手動觸發(fā)校驗。
三试吁、異步校驗器
如果說用戶手機校驗器需要檢查手機是否為黑名單的情況下惕橙,正常黑名單數(shù)據(jù)都存在遠程當中膝昆。這樣情況下需要發(fā)送HTTP請求辟汰,而這一過程就是異步廓潜。
Angular針對這類異步校驗有獨立的另一個標識符,即:NG_ASYNC_VALIDATORS
余耽,而其它代碼都是相通的缚柏。
@Directive({
selector: '[user-async]',
exportAs: 'userAsync',
providers: [{
provide: NG_ASYNC_VALIDATORS,
useExisting: forwardRef(() => UserAsyncDirective),
multi: true
}]
})
export class UserAsyncDirective implements Validator {
validate(c: AbstractControl): Observable<any> {
return c.valueChanges
// 去抖
.debounceTime(300)
// 抑制重復值
.distinctUntilChanged()
// 1、可以使用flatMap進行遠程校驗
// .flatMap(value => value)
// 2碟贾、本地模擬判斷
.map((value: string) => {
if ([ '15900000001', '15900000002' ].includes(value)) {
return {
mobile: {
msg: '手機號為黑名',
actualValue: value
}
}
}
return null;
})
.first();
}
}
除了 NG_ASYNC_VALIDATORS
核心的結構完全沒有變動币喧。
而對于 validate
方法返回的是一個 Observable
類型,利用對 valueChanges
的訂閱可以制作一些像去抖動作袱耽。
而最后必須使用 first()
做為結尾杀餐,原因每一次校驗,對于結果而言只允許一個朱巨。
結論
本章介紹的是如何對模板驅動表單創(chuàng)建自定義校驗器史翘,它相比較響應式表單自定義校驗器略為復雜一些。但是實際運用中冀续,我們不應該只為某個構建表單風格做一種自定義校驗器琼讽,應該二者是共存的。
比如上面 159 開頭的示例洪唐。更合理的編寫方式應該是將校驗邏輯獨立:
export class MyValidators {
static checkMobile(value: string): ValidationErrors|null {
return !value.startsWith('159') ? { mobile: { msg: '手機號必須是159開頭' } } : null;
}
}
// 校驗器類
export class UserMobileDirective implements Validator {
validate(c: AbstractControl): { [key: string]: any; } {
let value: string = c.value || '';
return MyValidators.checkMobile(value);
}
}
這樣钻蹬,同一個校驗器,不管是模板驅動表單還是響應式表單凭需,都能是通用的脉让。
Happy coding!