Angular如何在模板驅動表單中自定義校驗器

引言

模板驅動表單相比較響應式表單能夠少更少的代碼作一樣的事情,可也損失了自由度更易測試,固然不少人並不在意啦。html

因此我相信不少人在編寫Angular不自由自主去更傾向於模板驅動表單的寫法。git

表單最核心的是校驗體驗,在Angular中簡直就是發揮到了極致,好比:requiredminmaxpattern 等,這些本來是HTML DOM元素中的表述,而Angular默認實現了一整套的校驗指令,好比:required 對應 RequiredValidatorgithub

而後不少時候咱們須要一些特殊的校驗,好比:數據比較、遠程校驗等。那在模板驅動表單風格中咱們要如何優雅的實現這樣一個校驗器呢?typescript

1、Angular是如何校驗?

通常在編寫一個手機文本框多是這樣:app

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

以上幾行很友好的實現從必填項、格式進行校驗,而這一切都是依靠 [(ngModel)] 統一採集,得以只須要利用一個模板引用變量訪問到每一個校驗指令的錯誤信息。異步

一、[(ngModel)] 到底作了什麼?

在解析這個問題前須要先了解一下 RequiredValidator 是如何定義的。async

@Directive({
  providers: [{
      provide: NG_VALIDATORS,
      useExisting: forwardRef(() => RequiredValidator),
      multi: true
    }]
})
export class RequiredValidator {}

只看最核心向 NG_VALIDATORS 標識符註冊一個 RequiredValidator 指令。這樣就可使 ngModel 指令中注入 NG_VALIDATORS 後就能獲得這個指令對象。ide

ngModel 我把它簡化了一下:測試

export class NgModel extends NgControl {
    constructor(@Inject(NG_VALIDATORS) validators: Array<Validator|ValidatorFn>) {}
    
    get validator(): ValidatorFn|null {
        // 各類校驗並返回結果
    }
}

有關更多ng_model.ts能夠深刻閱讀源代碼。ui

Angular會在每一次表單值變動時,對全部的表單中已經安裝的校驗器進行一次遍歷。

2、編寫一個校驗器

誠如 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;
    }
}

只須要實現 Validator 接口的 validate 方法便可。

c 中獲取DOM值,當遇到非 159 開頭時,返回一個用於表述消息的對象便可,不然返回一個 null。這個對象會被統一採集在 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 可選方法,當某些其它外部屬性的變動時,容許從新手動觸發校驗。

3、異步校驗器

若是說用戶手機校驗器須要檢查手機是否爲黑名單的狀況下,正常黑名單數據都存在遠程當中。這樣狀況下須要發送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()
                // 一、可使用flatMap進行遠程校驗
                // .flatMap(value => value)
                // 二、本地模擬判斷
                .map((value: string) => {
                    if ([ '15900000001', '15900000002' ].includes(value)) {
                        return {
                            mobile: {
                                msg: '手機號爲黑名',
                                actualValue: value
                            }
                        }
                    }
                    return null;
                })
                .first();        
    }
}

除了 NG_ASYNC_VALIDATORS 核心的結構徹底沒有變更。

而對於 validate 方法返回的是一個 Observable 類型,利用對 valueChanges 的訂閱能夠製做一些像去抖動做。

而最後必須使用 first() 作爲結尾,緣由每一次校驗,對於結果而言只容許一個。

結論

本章介紹的是如何對模板驅動表單建立自定義校驗器,它相比較響應式表單自定義校驗器略爲複雜一些。可是實際運用中,咱們不該該只爲某個構建表單風格作一種自定義校驗器,應該兩者是共存的。

好比上面 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!

相關文章
相關標籤/搜索