Angular 深入淺出三十天:表單與測試 Day26 - 進階表單開發技巧 - 自訂驗證器

Day26

之前在開發表單的時候,我們都是使用 Angular 所提供的驗證器來驗證表單欄位裡的值是否符合我們的需求。

雖然 Angular 已經這麼貼心地提供了這麼多驗證了,但每個國家、地區的人文風土民情都不同,還有太多太多需要我們自己自訂規則才能符合需求的情況。

因此,今天我們就一起來看看要怎麼自訂驗證器吧!

自訂驗證器的型別

既然要自訂驗證器,就不能不知道驗證器的型別與其定義。

其實之前在第三天的文章:Reactive Forms 實作 - 以登入為例 裡就有提到驗證器的型別。

ValidatorFn

驗證器的型別是 ValidatorFn ,其原始碼定義如下:

1
2
3
interface ValidatorFn {
(control: AbstractControl): ValidationErrors | null
}

從定義中我們可以知道,驗證器其實就只是一個函式,該函式會傳入一個型別為 AbstractControl 的參數來讓我們在函式中判定該欄位的值是否符合我們的需求。

如果驗證結果符合需求,那就回傳 null ,代表沒有任何的錯誤;如果驗證結果不符合需求那就回傳一個型別為 ValidationErrors 的錯誤。

ValidationErrors 又是什麼呢?

ValidationErrors

ValidationErrors 之前最早是在第二天的文章: Template Driven Forms 實作 - 以登入為例 裡登場。

其原始碼定義如下:

1
2
3
type ValidationErrors = {
[key: string]: any;
};

沒錯,你沒看錯,就是這麼簡單!

從定義上看起來,基本上只要是個物件,就符合該型別的要求,而這也是因為滿足客製的條件,讓使用 Angular 的開發者有程度的規範但擁有盡可能大的彈性。

不過雖然大家可以隨意自訂,但我非常建議大家在自訂的時候可以參考官方的驗證器。

舉例來說,官方的 Validators.required 驗證器在驗證有誤時,會回傳的 ValidationErrors 是:

1
{ required: true }

Validators.pattern 驗證器在驗證有誤時,會回傳的 ValidationErrors 是:

1
2
3
4
{
actualValue: 'xxx',
requiredPattern: 'xxx'
}

Validators.minlengthValidators.minlength 驗證器在驗證有誤時,則會回傳:

1
2
3
4
{
actualLength: 1,
requiredLength: 2
}

我們從中不難發現官方會在 ValidationErrors 中,回傳該欄位當前的狀態以及需求的狀態;而物件的屬性名稱也會按照 actual 加上 XXXX 以及 required 加上 XXXX 的方式來命名。

雖然具體上還是要看實際需求,但我個人覺得我們自己在自訂驗證器的 ValidationErrors 時,也可以照著這個規則來處理。

一方面,整個系統會比較一致;另一方面,也不需要多寫太多額外的程式來處理我們自訂的錯誤。

舉個例子,如果我們想自訂一個欄位的值只能是 Leo 的驗證器,其程式碼可能會像是這樣:

1
2
3
4
5
6
7
8
9
10
export leoValidator: ValidatorFn = (control: AbstractControl) => {
const isLeo = control.value === 'Leo';
if (isLeo) {
return null
}
return {
actualValue: control.value,
requiredValue: 'Leo'
};
};

如此我們就在需要用到它時,直接像這樣使用即可:

1
new FormControl('', leoValidator);

又或者是彈性一點,讓使用它的人來決定該欄位的值只能是什麼,其程式碼應該會像是這樣:

1
2
3
4
5
6
7
8
9
10
11
export function nameValidator(name: string): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
if (control.value === name) {
return null
}
return {
actualValue: control.value,
requiredValue: name
};
};
}

然後就可以像這樣使用:

1
new FormControl('', nameValidator('Leo'));

不過, ValidatorFn 是用同步的方式來執行驗證,萬一遇到需要非同步驗證的情況要怎麼辦?

非同步驗證器

大家應該都滿喜歡玩遊戲的吧?!

謎之音:不要自己愛玩就認為別人都愛玩

絕大多數的遊戲,尤其是線上遊戲,在取名時不能夠取跟別人相同的名字,而當取到跟別人一樣的名字時,系統會提示「此名稱已被使用」之類的錯誤訊息。

面對這種應用場景,相信大部分的朋友可能會是使用 valueChanges 來訂閱欄位的變化事件,當使用者輸入名稱時,會呼叫 API 讓後端來幫忙驗證該名字是否已被使用,然後再根據回傳結果來決定是否顯示錯誤訊息。

畢竟前端不可能事先取得幾千、幾萬甚至是幾十萬、幾百萬的名字再一一比對吧?

不過如果有這樣的應用場景,我個人覺得還滿適合使用非同步驗證器來處理的。

順帶一提,這只是我個人舉例,不代表真實應用情況。

AsyncValidator

1
2
3
4
5
6
7
interface AsyncValidator extends Validator {
validate(control: AbstractControl): Promise<ValidationErrors | null> | Observable<ValidationErrors | null>

// inherited from forms/Validator
validate(control: AbstractControl): ValidationErrors | null
registerOnValidatorChange(fn: () => void)?: void
}

從上述定義可以看出,跟同步的驗證器所不一樣的是,我們在自訂非同步的驗證器時,不是直接製作一個符合 AsyncValidatorFn 定義的函式

而是要用一個可被注入的 Class 來實作 AsyncValidator 這個介面。

就像下面這個官網的範例一樣:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Injectable({ providedIn: 'root' })
export class UniqueAlterEgoValidator implements AsyncValidator {
constructor(private heroesService: HeroesService) {}

validate(
ctrl: AbstractControl
): Promise<ValidationErrors | null> | Observable<ValidationErrors | null> {
return this.heroesService.isAlterEgoTaken(ctrl.value).pipe(
map(isTaken => (isTaken ? { uniqueAlterEgo: true } : null)),
catchError(() => of(null))
);
}
}

使用方式

為我們欄位設定非同步驗證器的方式也非常地簡單。

FormControl 來說, 我們可以用這樣子的方式來設定:

1
new FormControl('', [/* 一般驗證器 */], [/* 非同步驗證器 */]);

簡單來說,不論是用 Reactive Forms 的哪種方式建立欄位,非同步驗證器都是放在一般驗證器後面就對了!

本日小結

希望透過今天的分享,能讓大家可以初步掌握自訂驗證器的技巧。雖然在實務上,大家不一定遇的到需要使用非同步驗證器的場景,但如果真的有需要用到又忘記怎麼做時,至少有這篇文章在,隨時都可以回來查詢。

此外,雖然沒有分享自訂 Template Driven Forms 驗證器的方式,但大家可以自行參考官方的 Form Validation - Adding custom validators to template-driven forms 文件。

而在非同步驗證器的部份,官網也有提到一些優化非同步驗證器的技巧,大家可以參考官方的 Form Validation - Optimizing performance of async validators 文件。

以上,就是今天的文章,如果有任何的問題或是回饋,還請麻煩留言給我讓我知道,感激不盡!

評論

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×