iT邦幫忙

2021 iThome 鐵人賽

DAY 30
1
Modern Web

Angular 深入淺出三十天:表單與測試系列 第 30

Angular 深入淺出三十天:表單與測試 Day30 - 表單原理

Day30

經過前面二十九天的的練習與學習,相信大家應該在表單的實作上都熟悉了不少,只要不是太複雜、太特別的表單應該也都難不倒你們。

今天是本系列文的最後一天,就讓我們來好好地深入了解一下 Angular 表單會這麼強大的原因吧!

首先,我想趁大家記憶猶新時,先帶大家來看為什麼我們昨天可以用 ControlContainer 來自訂一個可以被 Template Driven Forms 或是 Reactive Forms 所使用的 Component 。

ControlContainer

Diagram

上圖是我根據 Angular 的 Source Code 找出 ControlContainer 的關係所畫的(斜體表抽象類別)。

從圖中我們會發現, ControlContainer 其實只是一個抽象類別,並繼承了另一個抽象類別 AbstractControlDirective ,而 AbstractControlDirective 這個抽象類別其實也被另一個抽象類別 NgControl 所繼承。

NgControl 晚點會提到,此處暫不多做說明。

至於 ControlContainer ,它其實也被 AbstractFormGroupDirectNgFormFormGroupDirectiveFormArrayName 這四個 Directive 所繼承;甚至 AbstractFormGroupDirect 還被 FormGroupNameNgModelGroup 這兩個 Directive 所繼承。

換句話說, Angular 根據 ControlContainer 為基底,做出了以下五個 Directive :

  • FormGroupDirective
  • FormGroupName
  • FormArrayName
  • NgForm
  • NgModelGroup

在這五個 Directive 裡,前面三個是為什麼我們在用 Reactive Forms 的方式來開發表單時,可以在 Template 裡用 [formGroup][formGroupName][formArrayName] 的方式將元素與 FormGroupFormArray 綁定的原因。

大家應該都還記得我們是怎麼將 FormGroupFormArray 綁定到元素上的吧?!

而後面兩個則是為什麼我們在用 Template Driven Forms 的方式來開發表單時,可以在 Template 裡在元素上使用 #XXX="ngForm'#XXX="ngModelGroup" 之後,可以拿到 NgFormNgModelGroup 的實體的原因。

雖然本系列文沒有特別提到 ngModelGroup 的用法,想知道的朋友可以參考官方的 NgModelGroup API 文件

那為什麼我們可以使用 ControlContainer 來自訂元件呢?

其實這正是因為上述五個 Directive 都透過 ControlContainer 這個令牌,把自己註冊到 Angular 的 DI 系統裡,讓想使用它們的類別,可以很方便地透過 Angular 的 DI 系統來找到它們的實體。

像是在昨天的文章裡所分享的那樣(Reactive Forms 的方式):

export class AddressInfoComponent {
  constructor(private controlContainer: ControlContainer) { }
}

Template Driven Forms 則是透過 viewProvider 的方式,忘記的話請看昨天的文章

NgControl

Diagram

同樣地, Angular 也根據 NgControl 為基底,做出了以下三個 Directive :

  • FormControlName
  • FormControlDirective
  • NgModel

在這三個 Directive 裡,前面兩個是為什麼我們在用 Reactive Forms 的方式來開發表單時,可以在 Template 裡用 [formControl][formControlName] 的方式將元素與 FormControl 綁定的原因。

大家應該都還記得我們是怎麼將 FormControl 綁定到元素上的吧?!

而最後一個則是為什麼我們在用 Template Driven Forms 的方式來開發表單時,會在 Template 裡在元素上使用 #XXX="ngModel' 之後,可以拿到 NgFormNgModelGroup 的實體的原因。

不過,大家還記不記得我們在第二十八天的時候,是怎麼自訂表單元件的嗎?

沒錯!就是 ControlValueAccessor

上述三個 Directive 實作時,也是透過 DI 拿到實作了 ControlValueAccessor 介面的實體,並在初始化的時候透過 ControlValueAccessor 這個介面,搭建 FormControl 與實作了它的實體之間的溝通管道。

想看原始碼的朋友可以點我看原始碼。

如果想知道 @Self@Optional 裝飾器是幹嘛用的,可以參考這篇很前顯易懂的文章: @Self or @Optional @Host? The visual guide to Angular DI decorators.

但除了 ControlValueAccessor 之外,其實 Angular 還有其他內建的 ValueAccessor:

Diagram

從上圖中我們可以發現,內建的 ValueAccessor 基本上都是繼承於 BaseControlValueAccessor 這個類別,然後分別被 DefaultValueAccessorBuiltInControlValueAccessor 繼承,而只有 DefaultValueAccessor 有實作 ControlValueAccessor 這個介面。

然後 Angular 再基於 BuiltInControlValueAccessor 之上去建立了以下六個 ValueAccessor:

  • NumberValueAccessor
  • RangeValueAccessor
  • RadioControlValueAccessor
  • CheckboxControlValueAccessor
  • SelectControlValueAccessor
  • SelectMultipleControlValueAccessor

而這六個 ValueAccessor 對於我們的表單開發來說是至關重要的存在,沒有了它們,我們就沒辦法在這些元素上綁定我們的表單控制項。

但其實 BaseControlValueAccessorBuiltInControlValueAccessor 本身並沒有什麼比較特別的實作或定義,之所以會有 DefaultValueAccessorBuiltInControlValueAccessor 的區別,是為了在機制上,能夠做到一個優先權判斷的機制。

當我們在使用自訂的 ValueAccessor 時候, DefaultValueAccessor 或是上述六個 ValueAccessor 其實都有可能與我們自訂的 ValueAccessor 同時存在。

因此,在將我們的表單控制項與元素綁定的時候, Angular 會根據以下的優先權來抓取對應的 ValueAccessor :

  1. CustomValueAccessor
  2. BuiltInValueAccessor
  3. DefaultValueAccessor

如此一來,只要我們沒有自訂 ValueAccessor ,預設就是會使用內建的 ValueAccessor 來搭建表單控制項與元素之間的溝通橋樑。

我覺得 Angular 的開發團隊真的很聰明!

同步與非同步

除了上述的東西之外,其實還有一個比較特別的點,就是關於同步更新與非同步更新的問題。

Day16 - Template Driven Forms vs Reactive Forms 的小結裡,我分享了一個表格,表格裡提到了 Template Driven Forms 的可預測性是非同步的。

而這件事情其實可以在 NgModel原始碼第 222 行 看出一點端倪。

原始碼第 222 行這行是指 NgModel 在初始化的時候,會去設置表單控制項。

而在原始碼第 268 行中可以看到,它會判斷該 NgModel 是不是 _isStandalone ,也就是它是不是單獨存在,還是有被 <form></form> 包住。

如果是單獨存在, NgModel 的行為其實會跟 FormControlDirectiveFormControlName 一樣,因為他們會用一樣的方法設置表單控制項。

但如果不是單獨存在,它會用 NgFormaddControl 來設置表單控制項,這時,它就會是非同步的,因為他必須等到 resolvedPromise 發出 resolver 事件的時候,才會進行設置。

addControl 的實作在原始碼的第 187 行到 196 行

resolvedPromise 的定義則是在原始碼的第 27 行

我其實不太懂 Angular 為什麼要在 NgModel<form></form> 包起來的時候這樣子做,因為 resolvedPromise 其實也沒什麼特別的地方,但它們就讓 NgFormaddControlremoveControladdFormGroupremoveFormGroup 這四個方法要變成非同步的方式處理。

如果大家有興趣,或許找個時間研究一下,說不定,你就幫官方解決了一個問題呢!

本日小結

今天主要是幫本系列文做個結尾,希望能讓大家透過我的分享,更熟悉、更了解 Angular 一點。

也因為分享的關係,其實在撰寫本系列文的同時,我也得到了許多。

以前尚不熟悉的更熟悉了;以前不知道的知道了。

這或許就是所謂的「施比受更有福」吧?!

未來,還會不會再寫鐵人賽還不曉得(目前是不想再寫了,哈哈!)。

但是,寫鐵人賽真的是一個非常好的學習機會。

透過撰寫文章,來疏理自己所學,仔細咀嚼後再回饋給社群、回饋給社會。

如果你還沒寫過鐵人賽,我衷心推薦你這一輩子一定至少要寫一次鐵人賽。

最後,我想感謝訂閱我的文章、閱讀我的文章、喜歡我的文章的你們,謝謝你們不嫌棄,也謝謝你們願意讓我能夠幫到你們。

我也想感謝我的家人們,寫鐵人賽的這段期間,真的是非常地疏於陪伴,謝謝他們的支持(雖然他們看不到)。

感謝大家的收看,我們有緣再見! :)

其他資源


上一篇
Angular 深入淺出三十天:表單與測試 Day29 - ControlContainer
系列文
Angular 深入淺出三十天:表單與測試30
0
tso1158687
iT邦新手 4 級 ‧ 2021-10-15 23:35:40

推!

Leo iT邦新手 3 級 ‧ 2021-10-15 23:40:34 檢舉

謝啦!

0
鱈魚
iT邦新手 5 級 ‧ 2021-10-15 23:38:41

恭喜完賽!

Leo iT邦新手 3 級 ‧ 2021-10-15 23:40:21 檢舉

感謝鱈魚!

0
AlanShun
iT邦新手 5 級 ‧ 2021-10-15 23:43:10

恭喜完賽!

Leo iT邦新手 3 級 ‧ 2021-10-15 23:44:48 檢舉

感謝 Alan ^^

0
孤獨一隻雞
iT邦新手 4 級 ‧ 2021-10-16 09:20:59

來朝聖大神的文章

Leo iT邦新手 3 級 ‧ 2021-10-16 21:45:03 檢舉

客氣了

0
黃升煌 Mike
iT邦新手 2 級 ‧ 2021-10-16 11:26:28

恭喜大大完賽~~可以準備寫書了 /images/emoticon/emoticon33.gif

Leo iT邦新手 3 級 ‧ 2021-10-16 21:45:38 檢舉

感謝 Mike 大大

/images/emoticon/emoticon33.gif

我要留言

立即登入留言