我們知道,@angular/forms 包主要用來解決表單問題的,而表單問題非常重要的一個功能就是表單校驗功能。資料校驗非常重要,不僅僅前端在發請求給後端前需要校驗資料,後端對前端發來的資料也需要校驗其有效性和邏輯性,尤其在存入資料庫前還得校驗資料的有效性。 @angular/forms 定義了一個 Validator 介面,並內建了 RequiredValidator、CheckboxRequiredValidator、EmailValidator、MinLengthValidator、MaxLengthValidator、PatternValidator 六個常用的校驗指令,每一個 validator 都實現了 Validator 介面。這些校驗指令的使用很簡單,比如使用 EmailValidator 和 RequiredValidator 指令來校驗輸入的資料得是 email 且不能為空:
<input type="email" name="email" ngModel email required>
複製程式碼
這樣輸入的如果不是 email 格式,EmailValidator 指令就會校驗錯誤,會給 host(這裡也就是 input 元素)新增 'ng-invalid' class,這樣開發者可以給這個 class 新增一些 css 效果,提高使用者體驗。那麼,其內部執行過程是怎樣的呢?
實際上,上面 demo 中不僅僅繫結了 NgModel 指令,還繫結了 EmailValidator 和 RequiredValidator 兩個 validators 指令。指令在例項化時是按照宣告順序依次進行的,有依賴的指令則置後,FormsModule 先是宣告瞭 RequiredValidator 指令,然後是 EmailValidator 指令,最後才是 NgModel,所以例項化順序是 RequiredValidator -> EmailValidator -> NgModel,同時由於 NgModel 依賴於 NG_VALIDATORS,所以就算 NgModel 宣告在前也會被置後例項化。RequiredValidator 和 EmailValidator 在例項化過程中都會提供 REQUIRED_VALIDATOR 和 EMAIL_VALIDATOR 兩個服務,並且 StaticProvider 的 multi 屬性設定為 true,這樣可以容許有多個依賴服務(這裡是 RequiredValidator 和 EmailValidator 物件)公用一個令牌(這裡是 NG_VALIDATORS),multi 屬性作用可以檢視原始碼中說明。當 NgModel 例項化時,其構造依賴於 @Self() NG_VALIDATORS,@Self() 表示從 NgModel 指令掛載的宿主元素中去查詢這個令牌擁有的服務,NgModel 沒有提供 NG_VALIDATORS,但是掛載在 input 宿主元素上的 REQUIRED_VALIDATOR 和 EMAIL_VALIDATOR 卻提供了這個服務,所以 NgModel 的依賴 validators 就是這兩個指令組成的物件陣列。
NgModel 在例項化時,由於沒有父控制元件容器,所以會呼叫 _setUpStandalone(),從而呼叫 setUpControl() 方法設定 FormControl 物件的 同步 validator 依賴(如果有非同步 validator 依賴,也同理),這個依賴是呼叫 Validators.compose() 返回的一個 ValidatorFn 函式。而 Validators.compose() 引數呼叫的是 NgModel.validator,也就是呼叫 composeValidators 獲得 ValidatorFn,內部會呼叫 normalizeValidator() 函式轉換為為 (AbstractControl) => Validator.validate()。所以,和 input 控制元件繫結的 FormControl 物件就有了同步 validator 資料校驗器。那在 input 輸入框內輸入資料時,校驗器是在何時被執行的呢?
NgModel 例項化時,還安裝了一個 檢視資料更新回撥,這樣當 input 檢視內的資料更新時,就會執行這個回撥,該回撥會更新 FormControl 的 value 值,即 FormControl.setValue() 函式,內部會呼叫 updateValueAndValidity,從而開始 執行資料校驗器,上文說到 FormControl 的 validator 依賴實際上是 Validators.compose() 返回的函式,所以此時會執行 這個回撥函式,而這個 presentValidators 是 (AbstractControl) => RequiredValidator.validate() 和 (AbstractControl) => EmailValidator.validate() 組成的陣列,然後依次 執行 這兩個 Validator 的 validate() 函式。如果校驗錯誤,就返回 ValidationErrors,比如 email 校驗器返回的是 {'email': true}。這裡還需注意的是,Validator 指令裡的 validate() 函式實際上呼叫的還是 Validator 類 的對應的靜態函式,這樣驗證器指令可以直接在模板裡使用,而 Validator 類的靜態函式可以在 響應式表單 中使用。校驗器執行完成後,會設定 FormControl.errors 屬性,從而計算 FormControl 的 status 屬性,假設校驗錯誤,則 status 屬性值為 INVALID。那如果校驗錯誤,input 的 class 為何會新增 'ng-invalid' 呢?因為實際上還有一個 NgControlStatus 指令 也在繫結這個 input 元素,該指令的依賴會從當前掛載的宿主元素查詢 NgControl,本 demo 中就是 NgModel 指令,NgControlStatus 指令 的 host 屬性中的 '[class.ng-invalid]': 'ngClassInvalid',會執行 ngClassInvalid() 函式判斷是否會有 'ng-invalid' class,而校驗錯誤時,該函式執行結果是 true,因為它讀取的是 FormControl.invalid 屬性,則 'ng-invalid' class 就會被新增到 input 元素上。同理,其他 class 如 pending、dirty 等也同樣道理。這樣就理解了校驗器的整個執行過程,也包括為何校驗錯誤時會自動新增描述控制元件狀態的 'ng-invalid' class。
我們已經理解了 Validators 的內部執行流程,這樣寫一個自定義的 Validator 就很簡單了(當然,寫一個自定義的 Validator 不需要去了解 Validator 內部執行原理)。比如,寫一個自定義校驗器 ForbiddenValidator,input 輸入內容不能還有某些字串,那可以模仿 @angular/forms 中的內建校驗器 MinLengthValidator 寫法:
import {Validators as FormValidators} from '@angular/forms';
export class Validators extends FormValidators {
static forbidden(forbidden: string): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
return (new RegExp(forbidden)).test(control.value) ? {forbidden: true} : null;
}
}
}
export const FORBIDDEN_VALIDATOR: StaticProvider = {
provide: NG_VALIDATORS,
useExisting: forwardRef(() => ForbiddenValidator),
multi: true
};
@Directive({
selector:
':not([type=checkbox])[forbidden][formControlName],:not([type=checkbox])[forbidden][formControl],:not([type=checkbox])[forbidden][ngModel]',
providers: [FORBIDDEN_VALIDATOR],
})
export class ForbiddenValidator implements Validator{
private _onChange: () => void;
private _validator: ValidatorFn;
@Input() forbidden: string;
ngOnChanges(changes: SimpleChanges) {
if ('forbidden' in changes) {
this._createValidator();
if (this._onChange) this._onChange();
}
}
registerOnValidatorChange(fn: () => void): void {
this._onChange = fn;
}
validate(c: AbstractControl): ValidationErrors | null {
return this.forbidden ? this._validator(c) : null;
}
private _createValidator(): void {
this._validator = Validators.forbidden(this.forbidden);
}
}
複製程式碼
這樣就可以在元件模板中使用了:
@Component(
{
template: `
<h2>Template-Driven Form</h2>
<input type="email" name="email" [ngModel]="email" email required [forbidden]="forbiddenText">
<h2>Reactive-Driven Form</h2>
<input type="email" name="email" [formControl]="emailFormControl" email required [forbidden]="forbiddenText">
<h2>Update Forbidden Text</h2>
<input [(ngModel)]="forbiddenText">
`
})
export class AppComponent {
// custom validator
forbiddenText = 'test';
email = 'test@test.com';
emailFormControl = new FormControl('test@test.com', [Validators.forbidden(this.forbiddenText)]);
}
複製程式碼
完整程式碼可參見 stackblitz demo。
所以,在理解了 Validator 內部執行原理後,不僅僅可以寫自定義的 Validator,該 Validator 可以用於模板驅動表單也可以用於響應式表單,還能明白為啥需要那麼寫,這個很重要!
也可閱讀 @angular/forms 相關文章瞭解 NgModel 雙向繫結內部原理:@angular/forms 原始碼解析之雙向繫結。