經過前面二十九天的的練習與學習,相信大家應該在表單的實作上都熟悉了不少,只要不是太複雜、太特別的表單應該也都難不倒你們。
今天是本系列文的最後一天,就讓我們來好好地深入了解一下 Angular 表單會這麼強大的原因吧!
首先,我想趁大家記憶猶新時,先帶大家來看為什麼我們昨天可以用 ControlContainer
來自訂一個可以被 Template Driven Forms 或是 Reactive Forms 所使用的 Component 。
ControlContainer
上圖是我根據 Angular 的 Source Code 找出 ControlContainer
的關係所畫的(斜體表抽象類別)。
從圖中我們會發現, ControlContainer
其實只是一個抽象類別,並繼承了另一個抽象類別 AbstractControlDirective
,而 AbstractControlDirective
這個抽象類別其實也被另一個抽象類別 NgControl
所繼承。
NgControl
晚點會提到,此處暫不多做說明。
至於 ControlContainer
,它其實也被 AbstractFormGroupDirect
、 NgForm
、 FormGroupDirective
與 FormArrayName
這四個 Directive 所繼承;甚至 AbstractFormGroupDirect
還被 FormGroupName
與 NgModelGroup
這兩個 Directive 所繼承。
換句話說, Angular 根據 ControlContainer
為基底,做出了以下五個 Directive :
FormGroupDirective
FormGroupName
FormArrayName
NgForm
NgModelGroup
在這五個 Directive 裡,前面三個是為什麼我們在用 Reactive Forms 的方式來開發表單時,可以在 Template 裡用 [formGroup]
、 [formGroupName]
、 [formArrayName]
的方式將元素與 FormGroup
、 FormArray
綁定的原因。
大家應該都還記得我們是怎麼將 FormGroup
與 FormArray
綁定到元素上的吧?!
而後面兩個則是為什麼我們在用 Template Driven Forms 的方式來開發表單時,可以在 Template 裡在元素上使用 #XXX="ngForm'
、 #XXX="ngModelGroup"
之後,可以拿到 NgForm
與 NgModelGroup
的實體的原因。
雖然本系列文沒有特別提到 ngModelGroup
的用法,想知道的朋友可以參考官方的 NgModelGroup API 文件
那為什麼我們可以使用 ControlContainer
來自訂元件呢?
其實這正是因為上述五個 Directive 都透過 ControlContainer
這個令牌,把自己註冊到 Angular 的 DI 系統裡,讓想使用它們的類別,可以很方便地透過 Angular 的 DI 系統來找到它們的實體。
像是在昨天的文章裡所分享的那樣(Reactive Forms 的方式):
1 2 3
| export class AddressInfoComponent { constructor(private controlContainer: ControlContainer) { } }
|
Template Driven Forms 則是透過 viewProvider
的方式,忘記的話請看昨天的文章。
NgControl
同樣地, Angular 也根據 NgControl
為基底,做出了以下三個 Directive :
FormControlName
FormControlDirective
NgModel
在這三個 Directive 裡,前面兩個是為什麼我們在用 Reactive Forms 的方式來開發表單時,可以在 Template 裡用 [formControl]
、 [formControlName]
的方式將元素與 FormControl
綁定的原因。
大家應該都還記得我們是怎麼將 FormControl
綁定到元素上的吧?!
而最後一個則是為什麼我們在用 Template Driven Forms 的方式來開發表單時,會在 Template 裡在元素上使用 #XXX="ngModel'
之後,可以拿到 NgForm
與 NgModelGroup
的實體的原因。
不過,大家還記不記得我們在第二十八天的時候,是怎麼自訂表單元件的嗎?
沒錯!就是 ControlValueAccessor
。
上述三個 Directive 實作時,也是透過 DI 拿到實作了 ControlValueAccessor
介面的實體,並在初始化的時候透過 ControlValueAccessor
這個介面,搭建 FormControl
與實作了它的實體之間的溝通管道。
想看原始碼的朋友可以點我看原始碼。
如果想知道 @Self
與 @Optional
裝飾器是幹嘛用的,可以參考這篇很前顯易懂的文章: @Self or @Optional @Host? The visual guide to Angular DI decorators.
但除了 ControlValueAccessor
之外,其實 Angular 還有其他內建的 ValueAccessor:
從上圖中我們可以發現,內建的 ValueAccessor 基本上都是繼承於 BaseControlValueAccessor
這個類別,然後分別被 DefaultValueAccessor
與 BuiltInControlValueAccessor
繼承,而只有 DefaultValueAccessor
有實作 ControlValueAccessor
這個介面。
然後 Angular 再基於 BuiltInControlValueAccessor
之上去建立了以下六個 ValueAccessor:
NumberValueAccessor
RangeValueAccessor
RadioControlValueAccessor
CheckboxControlValueAccessor
SelectControlValueAccessor
SelectMultipleControlValueAccessor
而這六個 ValueAccessor 對於我們的表單開發來說是至關重要的存在,沒有了它們,我們就沒辦法在這些元素上綁定我們的表單控制項。
但其實 BaseControlValueAccessor
與 BuiltInControlValueAccessor
本身並沒有什麼比較特別的實作或定義,之所以會有 DefaultValueAccessor
與 BuiltInControlValueAccessor
的區別,是為了在機制上,能夠做到一個優先權判斷的機制。
當我們在使用自訂的 ValueAccessor
時候, DefaultValueAccessor
或是上述六個 ValueAccessor 其實都有可能與我們自訂的 ValueAccessor
同時存在。
因此,在將我們的表單控制項與元素綁定的時候, Angular 會根據以下的優先權來抓取對應的 ValueAccessor :
- CustomValueAccessor
- BuiltInValueAccessor
- DefaultValueAccessor
如此一來,只要我們沒有自訂 ValueAccessor ,預設就是會使用內建的 ValueAccessor 來搭建表單控制項與元素之間的溝通橋樑。
我覺得 Angular 的開發團隊真的很聰明!
同步與非同步
除了上述的東西之外,其實還有一個比較特別的點,就是關於同步更新與非同步更新的問題。
在 Day16 - Template Driven Forms vs Reactive Forms 的小結裡,我分享了一個表格,表格裡提到了 Template Driven Forms 的可預測性是非同步的。
而這件事情其實可以在 NgModel
的原始碼第 222 行 看出一點端倪。
原始碼第 222 行這行是指 NgModel
在初始化的時候,會去設置表單控制項。
而在原始碼第 268 行中可以看到,它會判斷該 NgModel 是不是 _isStandalone
,也就是它是不是單獨存在,還是有被 <form></form>
包住。
如果是單獨存在, NgModel
的行為其實會跟 FormControlDirective
與 FormControlName
一樣,因為他們會用一樣的方法設置表單控制項。
但如果不是單獨存在,它會用 NgForm
的 addControl
來設置表單控制項,這時,它就會是非同步的,因為他必須等到 resolvedPromise
發出 resolver
事件的時候,才會進行設置。
addControl
的實作在原始碼的第 187 行到 196 行
resolvedPromise
的定義則是在原始碼的第 27 行
我其實不太懂 Angular 為什麼要在 NgModel
被 <form></form>
包起來的時候這樣子做,因為 resolvedPromise
其實也沒什麼特別的地方,但它們就讓 NgForm
在 addControl
、 removeControl
、 addFormGroup
與 removeFormGroup
這四個方法要變成非同步的方式處理。
如果大家有興趣,或許找個時間研究一下,說不定,你就幫官方解決了一個問題呢!
本日小結
今天主要是幫本系列文做個結尾,希望能讓大家透過我的分享,更熟悉、更了解 Angular 一點。
也因為分享的關係,其實在撰寫本系列文的同時,我也得到了許多。
以前尚不熟悉的更熟悉了;以前不知道的知道了。
這或許就是所謂的「施比受更有福」吧?!
未來,還會不會再寫鐵人賽還不曉得(目前是不想再寫了,哈哈!)。
但是,寫鐵人賽真的是一個非常好的學習機會。
透過撰寫文章,來疏理自己所學,仔細咀嚼後再回饋給社群、回饋給社會。
如果你還沒寫過鐵人賽,我衷心推薦你這一輩子一定至少要寫一次鐵人賽。
最後,我想感謝訂閱我的文章、閱讀我的文章、喜歡我的文章的你們,謝謝你們不嫌棄,也謝謝你們願意讓我能夠幫到你們。
我也想感謝我的家人們,寫鐵人賽的這段期間,真的是非常地疏於陪伴,謝謝他們的支持(雖然他們看不到)。
感謝大家的收看,我們有緣再見! :)
其他資源