
大家在日常生活中,應該看過滿多表單的某個欄位會隨著另個欄位的改變,而造成該欄位的驗證邏輯需要改變的情況吧?
舉例來說,可能會有個欄位叫做聯絡資訊,使用者可以選擇要填入手機號碼或者是 E-mail ,該欄位再根據使用所選擇的類型來檢核該欄位的值。
今天,我們就來用 Reactive Forms 實作這個欄位,而這個欄位我會實作在我們的被保人表單上,各位就隨意吧!
如果已經忘記被保人表單長怎麼樣的話,可以先回頭複習一下第十一天的文章:Reactive Forms 實作 - 動態表單初體驗。
實作開始
首先,我們需要在原本的被保人表單裡新增一個欄位:聯絡資訊。
HTML 的部份大概會長這樣:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 
 | <p><label>聯絡資訊:</label>
 </p>
 <p>
 <select>
 <option value="">請選擇</option>
 <option value="mobile">手機</option>
 <option value="email">E-Mail</option>
 </select>
 <input type="text">
 </p>
 
 | 
畫面看起來會像這樣:

雖然聯絡資訊是一個欄位,但其實我們需要兩個 FormControl ,一個給下拉選單,一個給實際填值的 input 元素。
因此,我們要在原本的 createInsuredFormGroup 裡多加兩個欄位,像是這樣:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 
 | private createInsuredFormGroup(): FormGroup {return this.formBuilder.group({
 name: [
 '',
 [Validators.required, Validators.minLength(2), Validators.maxLength(10)]
 ],
 gender: ['', Validators.required],
 age: ['', Validators.required],
 contactInfoType: ['', Validators.required],
 contactInfo: ['', Validators.required]
 });
 }
 
 | 
然後將剛剛新增的欄位與畫面的元素綁定:
| 12
 3
 4
 5
 6
 7
 8
 
 | <p><select formControlName="contactInfoType">
 <option value="">請選擇</option>
 <option value="mobile">手機</option>
 <option value="email">E-Mail</option>
 </select>
 <input type="text" formControlName="contactInfo">
 </p>
 
 | 
接著我們透過把資料印在畫面上的方式來檢查是否已正確綁定,像這樣:
| 1
 | <pre>{{ formGroup?.getRawValue() | json }}</pre>
 | 
結果:

看起來已經有正確跟畫面上的元素綁定了,那接下來要怎麼做才好呢?
valueChanges
FormControl 的父類別 AbstractControl 有個屬性叫做 valueChanges ,它是一個 Observable 。
我們可以透過訂閱某個 AbstractControl 的 valueChanges 這個 Observable 來知道該欄位是否已經發生變化,並且做出相應的處理。
因此,我們可以這樣調整 createInsuredFormGroup 裡的實作:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 
 | private createInsuredFormGroup(): FormGroup {const contactInfoTypeControl = this.formBuilder.control('', Validators.required);
 const contactInfoControl = this.formBuilder.control('', Validators.required);
 contactInfoTypeControl.valueChanges.subscribe((value) => {
 switch (value) {
 case 'mobile':
 contactInfoControl.setValidators([Validators.required, Validators.pattern(/$09\d{8}^/)]);
 break;
 case 'email':
 contactInfoControl.setValidators([Validators.required, Validators.email]);
 break;
 default:
 contactInfoControl.setValidators([Validators.required]);
 break;
 }
 contactInfoControl.updateValueAndValidity();
 });
 
 return this.formBuilder.group({
 name: [
 '',
 [Validators.required, Validators.minLength(2), Validators.maxLength(10)]
 ],
 gender: ['', Validators.required],
 age: ['', Validators.required],
 contactInfoType: contactInfoTypeControl,
 contactInfo: contactInfoControl
 });
 }
 
 | 
上述程式碼中有以下三個要點:
- 建立 - FormControl的時候可以藉由- this.formBuilder.control()的方式建立,也可以直接使用- new FormControl()建立,這點在前面的文章已經有提過,不過我在這邊再提醒大家一次。
 
- setValidators()執行完後,記得一定要使用- updateValueAndValidity()來更新當前欄位的驗證,不然就要等到該欄位的值有改變時才會以新的驗證器來驗證。
 
- 由於 - contactInfoType允許使用者選擇- 請選擇的選項,因此記得在- default的區塊裡,將- Validators.required給加回去。
 
這邊改好之後,我們也順便調整一下 getErrorMessage 的實作,讓使用者可以知道該欄位的驗證有誤:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 
 | getErrorMessage(key: string, index: number): string {const formGroup = this.formArray.controls[index];
 const formControl = formGroup.get(key);
 let errorMessage: string;
 if (!formControl || !formControl.errors || formControl.pristine) {
 errorMessage = '';
 } else if (formControl.errors.required) {
 errorMessage = '此欄位必填';
 } else if (formControl.errors.minlength) {
 errorMessage = '姓名至少需兩個字以上';
 } else if (formControl.errors.maxlength) {
 errorMessage = '姓名至多只能輸入十個字';
 
 
 } else if (formControl.errors.pattern) {
 errorMessage = '手機號碼格式錯誤';
 } else if (formControl.errors.email) {
 errorMessage = 'E-mail 格式錯誤';
 }
 
 return errorMessage!;
 }
 
 | 
這邊要提醒大家的是,由於驗證 E-mail 格式的方式我今天是用 Validators.email 的驗證器來驗,不是之前的 Validators.pattern() ,所以我可以直接用 formControl.errors.email 來判斷。
如果實作時,手機號碼跟 E-mail 都是用  Validators.pattern() 的驗證器來驗的話,就需要進一步去比對 formControl.errors.pattern 裡的 Regular Expression 來分辨究竟是手機號碼的格式錯誤還是 E-mail 的格式錯誤了。
像是這樣:
| 12
 3
 4
 5
 6
 7
 8
 
 | } else if (formControl.errors.pattern) {const requiredPattern = formControl.errors.pattern.requiredPattern;
 if (requiredPattern === '/A Regular Expression/') {
 errorMessage = '手機號碼格式錯誤';
 } else if (requiredPattern === '/B Regular Expression/') {
 errorMessage = 'E-mail 格式錯誤';
 }
 }
 
 | 
如此一來,我們就完成這個欄位的功能囉!
結果:

本日小結
今天的重點是學會如何使用 valueChanges 來動態調整相關欄位的驗證邏輯。
雖然是 Observable 是 RxJS 的東西,但今天並沒有太艱難或太複雜的運用,使用上的感覺會跟使用 Promise 的感覺類似,不過我個人認為 RxJS 好玩且強大許多。
關於 RxJS ,如果大家想知道更多資訊,我推薦大家去看 Mike 的打通 RxJS 任督二脈系列文,或者是直接買實體書也行。
雖然今天的實作已經完成了,但因為有調整程式碼的關係,測試程式碼其實也需要相應的調整才不會出錯,此部份就交給大家實作我就不再用篇幅分享實作囉!
今天的程式碼會放在 Github - Branch: day23 上供大家參考,建議大家在看我的實作之前,先按照需求規格自己做一遍,之後再跟我的對照,看看自己的實作跟我的實作不同的地方在哪裡、有什麼好處與壞處,如此反覆咀嚼消化後,我相信你一定可以進步地非常快!
如果有任何的問題或是回饋,還請麻煩留言給我讓我知道!