經過了這段時間的練習與學習,相信大家應該越來越能體會 Angular 表單的強大與便利。
不過既然 Angular 表單這麼好用,如果能讓自己做的 Component 也像 Angular 表單那樣一般使用該有多好?
因此,今天想要跟大家分享的是 ─ 如何自訂表單元件。
應用場景
大家跟我一起想像一下,假設我們今天需要做一個管理平台,在這個管理平台裡,會有很多地方都會需要用到我們昨天做的 DateRangeComponent
,但不一定會是在同一個表單裡,只是剛好也需要 startDate
與 endDate
這兩個欄位,而且畫面與欄位驗證的規則也都是一樣。
例如:
A 頁面是一個查詢訂單系統, B 頁面是查詢會員系統,雖然這兩個頁面的查詢條件可能都不太一樣,但恰好都可以根據起迄日來查詢相應的資料。
這時,我們很有可能就會將我們做好的 DateRangeComponent
做成表單元件,讓 A 跟 B 在使用它的時候,就像使用一般的表單元件一樣輕鬆、自然。
那究竟要怎麼做呢?
ControlValueAccessor
首先要介紹給大家認識的是 ControlValueAccessor
,它是個 Interface
,而它定義了以下四個函式:
1 | interface ControlValueAccessor { |
writeValue(obj: any): void
─ 表單控件想要將值寫入時,會呼叫此函式registerOnChange(fn: any): void
─ 表單控件初始化時會呼叫此函式,並傳入一個回呼函式,讓實作此介面的類別在其值有變動時,使用該回呼函式並傳入欲變動的值registerOnTouched(fn: any): void
─ 表單控件初始化時會呼叫此函式,並傳入一個回呼函式,讓實作此介面的類別在失去焦點時,使用該回呼函式以通知表單控件setDisabledState(isDisabled: boolean)?: void
─ 當表單控件的狀態變成DISABLED
抑或是從DISABLED
改變成其他狀態時,會呼叫此函式以通知實作此介面的類別
雖然我覺得我說的滿清楚的,但大家應該還是覺得很模糊,對吧?
不要緊,我只是先讓大家有個印象,待會實作時大家就會更加理解了。
實作開始
首先,我們需要另一個 Component 來用我們昨天做好的 DateRangeComponent
,像這樣:
1 | <form *ngIf="formGroup" [formGroup]="formGroup"> |
然後在 Component 的 .ts
裡準備好 FormGroup
,像這樣:
1 | export class ReactiveFormsDateRangeComponent implements OnInit { |
接著打開昨天做的 DateRangeComponent
,並在 implements
的後方加上 ControlValueAccessor
,像這樣:
1 | export class DateRangeComponent implements OnInit, ControlValueAccessor { |
這時你應該會發現 DateRangeComponent
出現了一條紅色毛毛蟲,當你把滑鼠游標移到上面的時候,它說:
這是因為我們為 DateRangeComponent
加上實作 ControlValueAccessor
的宣告後,編輯器提醒我們要記得實作 ControlValueAccessor
的四個函式,才符合該介面的定義。
這就像是我們如果想要 Cosplay 鋼鐵人,但我什麼盔甲都沒穿就說自己是鋼鐵人,別人只會覺得滿臉問號。
但只要我們戴上了頭盔,別人就會知道你在扮演鋼鐵人。
所以我們就在 DateRangeComponent
裡加上以下四個函式:
1 | export class DateRangeComponent implements OnInit, ControlValueAccessor { |
接下來,我們需要在 DateRangeComponent
的 MetaData 裡的 providers
裡加入一些設定,像這樣:
1 | @Component({ |
我們之前其實也曾經在第二十五天的文章 ─ 測試進階技巧 - DI 抽換裡用過類似的技巧。
簡單來說,這個設定是為了讓表單可以透過 NG_VALUE_ACCESSOR
這個 InjectionToken 取得我們這個實作了 ControlValueAccessor
介面的 DateRangeComponent
實體。
想知道什麼是 InjectionToken 的朋友,可以參考 Mike 的 [Angular 大師之路] Day 23 - 認識 InjectionToken 。
想知道
useExisting
跟useValue
、useClass
與useFactory
有哪裡不一樣的,也可以參考 Mike 的 [Angular 大師之路] Day 20 - 在 @NgModule 的 providers: [] 自由更換注入內容 (1) 與 [Angular 大師之路] Day 21 - 在 @NgModule 的 providers: [] 自由更換注入內容 (2) 。而
forwardRef()
的部份,我覺得官網的 Dependency injection in action - Break circularities with a forward class reference 講得比較清楚。最後的
multi: true
,可以參考林穎平 EP 的 [Day 8] 所以我說那個 multi 是? ,如果想要更深入的了解其原理,他也寫了一篇 [Day 10] 深度看一下 Angular 建立 multi provider 的機制(真的很深入)
至此,我們就可以儲存檔案來看一下初始化完後會印出的 Log :
接著我們在使用 DateRangeComponent
的 Component 裡加上以下程式碼以觀察其運作結果:
1 | ngOnInit(): void { |
然後我們會發現:
這樣大家有比較了解一開始關於 ControlValueAccessor
各函式的說明了嗎?
如果用圖示的話,現在的結構大概像這樣:
如果我們設值給 FormControl
時,則會觸發 ControlValueAccessor
的函式 writeValue
:
如果我們 disable
或 enable
了該 FormControl
,則會觸發 ControlValueAccessor
的函式 setDisabledState
:
而如果使用者改動了自訂的表單元件的值,則我們自訂的表單元件應該要呼叫透過初始化時所觸發的 registerOnChange
所傳入的 fn
去通知 FormControl
:
讀萬卷書不如行萬里路。接下來,我們把剩下的實作做完就會更了解這其中的運作流程了!
首先,先加工一下使用 DateRangeComponent
的 Component :
1 | export class ReactiveFormsDateRangeComponent implements OnInit { |
Template 的部份也加工一下:
1 | <form *ngIf="formGroup" [formGroup]="formGroup"> |
然後把 DateRangeComponent
改成這樣:
1 | export class DateRangeComponent implements OnInit, ControlValueAccessor { |
結果:
對了,這樣的作法不僅僅只適用於 Reactive Forms 噢!大家可以在使用
DateRangeComponent
的時候用 Template Driven Forms 的方式試試看,也是行得通的唷!
本日小結
今天的實作練習應該滿好玩的吧?
我能理解大家第一次碰到的時候都會比較難以理解,記得我第一次碰到的時候,也只是複製人家的程式碼然後貼上而已,根本就不是了解其運作原理。
因此,希望我今天的文章能讓大家可以不僅僅只是複製貼上,而是對於其流程與原理有所掌握與理解。
今天的程式碼會放在 Github - Branch: day28 上供大家參考,建議大家在看我的實作之前,先按照需求規格自己做一遍,之後再跟我的對照,看看自己的實作跟我的實作不同的地方在哪裡、有什麼好處與壞處,如此反覆咀嚼消化後,我相信你一定可以進步地非常快!
如果有任何的問題或是回饋,還請麻煩留言給我讓我知道!