[功能介紹-10] Reactive Forms (Model-Driven Forms) 

Template-Driven Forms與Model-Driven Forms的比較

Reactive forms

Reactive forms的驗證大多是直接寫在controller裡的,會是一個明確的、非UI的data flowing。
Reactive forms的reactive patterns可以讓測試與驗證更加簡單。

使用Reactive forms可以用一個樹狀的控制物件來binding到表單template的元件上,這讓所有驗證的程式碼都集中在一起,方便維護與管理,在撰寫單元測試時也會較為容易。

使用Model-Driven Forms也較符合reactive programming的概念(延伸閱讀:Functional Reactive Programming 的入門心得

Template-driven forms

Template-driven forms是將組件驗證控制的功能寫在像是<input>或<select>的標籤內,並利用ngModel來確認是否輸入了合法的內容。
使用表單驅動驗證不需要自己創建control objects,因為angular已經為我們建好了。
ngModel會處理使用者改變與輸入表單的事件,並更新ngModel裡面的可變數據,讓我們可以去處理後續的事。
也因此ngModel並不是ReactiveFormsModule的一部份。

這代表著使用表單驅動驗證,我們需要撰寫的程式碼更少。
但是如果我們的表單需要很複雜的驗證步驟並且要顯示很多不同的錯誤訊息時,使用表單驅動驗證會使事情變得更複雜並難以維護。

最大的差異,同步與非同步

Reactive forms是同步的而Template-driven forms為非同步處理,是這兩者間最大的差異。

對Reactive forms來說,所有表單的資料是在code裡以tree的方式來呈現,所以在任一個節點可以取得其他表單的資料,並且這些資料是即時同步被更新的。我們也可以在使用者修改了某個input的值時,去為使用者自動update另一個input內的預設值,這是因為所有資料都是隨時可取得的。

Template-driven forms在每一個表單元件各自透過directive委派檢查的功能,為了避免檢查後修改而造成檢查失效的問題,directive會在更多的時後去檢查輸入的值的正確性,因此並沒有辦法立即的得到回應,而需要一小段的時間才有辦法得到使用者輸入的值是否合法的回應。這會讓我們在撰寫單元測試時更加複雜,我們會需要利用setTimeout去讓取得的檢查結果是正確的

使用Reactive Forms

Reactive Forms的功能封裝在ReactiveFormsModule中,和FormsModule同樣在@angular/forms之下。
如果要使用Reactive Forms需要使用下面的程式碼

Reactive Forms的四個重要成員

  • AbstractControl:AbstractControl是FormControl、FormGroup、FormArray這三個實例表單類的抽象基類。它提供了他們的通用行為以及屬性,例如observable。
  • FormControl:在單個表單元件中檢查值並驗證狀態(比如input、select等等)。
  • FormGroup一組值與驗證狀態(FormControl),其屬性包含了它們的子控件。例如一個form表單就是一個FormGroup。
  • FormArray:用索引的方式去追蹤檢查表單的驗證狀態。

Add a FormGroup

首先要新增FormGroup所需使用的類別

然後創建這個Group,並定義裡面的驗證元素

接著,在template裡面的form裡指定這個form要使用heroForm來做表單驗證,並且在input裡面指定他的formControlName

form標籤下的novalidate屬性,是為了要防止瀏覽器自己執行native的驗證。

[formGroup]=”heroForm”則是將template內的form元件與controller裡所創的formGroup做關連

這個則是將input與formGroup下名為name的formControl做關連。

註:bootstrap的form-group以及form-control與angular完全無關。下面是bootstrap為我們設計的form表單樣式範例,但是這只是css,沒辦法讓組件與控制器結合。

Introduction to FormBuilder

FormBuilder可以減少我們在創建formGroup時有太多重覆的定義,要使用要先import必要的檔案

使用formBuild大致要做的事如下:

  • 宣告heroForm為FormGroup
  • 在初始化元件時inject FormBuilder
  • 創建form控件時需要另外去呼叫函數createForm,使用注入的FormBuilder來創建formControl
  • 在創建formGroup時,用this.fb.group來宣告這個formGroup裡所有的formControl。

formBuild的宣告方式如上,name控件由其初始數據值(一個空字符串)定義。

使用驗證器

首先要先import該驗證器

然後在建立formControl時指定使用該驗證器

Nested FormGroups

有時我們在做地址輸入框時,會有如國家、區、鄉、市、街、郵遞區號等不同的輸入欄位,但他們應該是一個group,這時候就可以用nested formGroup。透過這樣的結構層次,可以讓我們在追蹤表格狀態更為容易清楚。

我們用一個div將整個地址的區塊包起來,並用formGroupName=”address”來與heroForm裡的address做連結

檢視FormControl裡的屬性

我們可以用下面的方式來將formControl裡可使用的屬性都印出來

基本上會有下面四個屬性可以讓我們使用

屬性 描述
myControl.value FormControl使用者輸入的值
myControl.status 這個FormControl的驗證結果. 可能的值有: VALIDINVALIDPENDING, or DISABLED.
myControl.pristine 使用者是否有在UI上更動過這個元素,假如沒有的話會是true。相反的屬性為myControl.dirty.
myControl.untouched 使用者尚未輸入並且從未觸發過blur event時為true。相反的屬性為 myControl.touched.

The data model and the form model

以往我們在創建資料類型時是像這樣子的

但是我們在創建formGroup是這樣子的

我們可以直接利用class來創建formControl

使用setValue和patchValue為表單填入初始值

在上面data model和form model的介紹範例中,可以看到Hero與formGroup建立heroForm模型有兩個顯著的區別:

  • Hero class有id,formGroup沒有。
  • Hero class的地址是一個陣列

但是我們可以利用setValue來更簡單的將一個class的資料填進表單中。

也可以使用patchValue來將單一的值填入表單裡

如果我們要做一個修改hero資料的列表,當點下某個hero時就可以修改該hero的資料

然後在controller裡面去監聽ngOnChange事件並且用setValue來設定要修改的值

會需要使用reset是為了要清除前一個hero的資料

使用FormArray來呈現一個FormGroups

如果一個hero可能需要有多組的地址時,就會需要使用formArray。
原本我們是這樣定義Address的

使用formArray則變成這樣

可以用下面的function將很多組的address設定進去formArray成為預設值

要取得formArray可以撰寫下面的方法

而顯示方式如下:

完整內容如下:

要為這個hero新增一個地址可以用下面這個方法

按下增加地址按鈕時呼叫這個方法

將formControl的資料用深層複製存回class裡的方法

參考資料

[功能介紹-9] Template-Driven Forms

使用Event由template表單傳送資料給Component

利用事件的$event去傳送相關資訊給component

接收則可以透過event.target去存取該htmlInputElement的資料(詳細資料請見此
下面的範例能將value存至一個變數內且顯示在頁面上:

模版驅動的表單

使用模版驅動的表單需要在app.module.ts裡面宣告我們要使用FormsModule這個library,宣告方式如下

使用ngModel對input的值與controller裡的變數做雙向繫結

下面這段code最主要要看的是[(ngModel)]="model.name",用這個標籤會自動將使用者輸入此input的值雙向綁定到model.name上面。

然後在component也要有model這個屬性才能夠被綁定

成果如下:

ngModel會附加的class

狀態(State ) Class if true Class if false
被點擊接觸過 ng-touched ng-untouched
值被改變 ng-dirty ng-pristine
值不符合驗證 ng-valid ng-invalid

可以用下面的範例來觀察input元素class名稱的改變(#spy代表將這個input存到一個叫做spy的變數內)


下面是類別改變的狀態表

Show and hide validation error messages

在做表單驗證時,時常會有如下圖這樣的需求

若要使用表單驗證,需要在input宣告ngModel(不論有沒有做雙向綁定),或者宣告formControlName或formControl,不然無法驗證。
下面是一個最簡單的required驗證宣告:

如果要用客製化的訊息去告訴使用者那邊輸入錯誤的話,則可以將ngModel的值輸出成一個模版變數#name

在上面我們看到#name="ngModel",這是因為ngModel這個directive的exportAs的值剛好就是ngModel。
這代表將input的ngModel存進一個叫做name的變數裡,下面就可以使用這個ngModel的模型顯示相關的錯誤訊息。

pristine的意思是未被改變過的,而name.valid則是檢查所有我們所設定的驗證器,回傳是否合法或者不合法。
因此[hidden]="name.valid || name.pristine"的意思就是如果尚未填到該選項或者填入的值是合法的,則不顯示錯誤訊息。

現成的forms Validator

在上面的例子中,我們在input中下了required來做必填欄位的驗證。
required是一個directive,我們可以在這邊看到這個directive的詳細使用說明。
在說明網頁中,有幾個是需要去看的,首先就是selector,代表我們要如何去使用這個驗證器。
以required為例,有三種選擇required的方法如下圖:

這個圖代表了required不能用在checkbox,要使用這個directive要在input上加上required以及ngModel(或者formControl, formControlName)屬性。

其他Angular有提供的驗證器如下

客製化模板驅動表格驗證器

除了上述的內建驗證器之外,我們也可以自己製作自製的驗證器directive

在使用上的部份,就可以直接像下面這樣

要注意的是,官網範例forbidden-name.directive.ts的selector名稱寫錯,不能使用appForbiddenName,要用forbiddenName,才能對的起來
這邊有ISSUE回報: Custom Validator example on angular.io directive selector issue.
詳情請見線上範例:live example

使用ngSubmit送出表單

在ngForm裡面,submit的button按下後並不會直接發生任何事,而是會觸發一個ngSubmit的事件,因此需要在form的地方去註冊ngSubmit事件

在上面的程式碼裡,我們將這個表單ngForm存至heroForm這個變數裡,因此在submit的按鈕可以存取ngForm裡的值來判段這個表單是否已通過驗證

參考資料

[功能介紹-8] Pipes

Pipes有那些?

常用的Pipes有DatePipeUpperCasePipeLowerCasePipeCurrencyPipe,和PercentPipe。它們都可用於任何模板。

下面是angular內建所有的的Pipes說明:

如何使用Pipes

管道將數據作為輸入並將其轉換為所需的輸出。下面是使用DatePipe的範例。

在Pipes裡使用參數

請見下面的範例

這樣會顯示結果如下:
MM/dd/yy:04/15/88
shortDate:04/15/1988
fullDate:Friday, April 15, 1988

串接多個管道

下面為一個例子

結果會顯示
FRIDAY, APRIL 15, 1988

定義自己的Pipes

下面是一個自製Pipes的例子:

在上面的例子中可以看到

  • 會以@Pipe({name:’XXXX’})來宣告這個class是一個pipe
  • pipe類別需implements PipeTransform介面並依照要input的值來選擇要實作的transform方法
  • transform有一個可選參數exponent,可讓使用者帶要帶入的參數進Pipes
  • Pipe的名字需要是一個合法的Javascript命名

下面是一個使用範例

顯示的結果如下:

在每次被綁定的值更動時,都會再跑一次Pipes的功能,一般來說,Pipe只會偵測值的變化才會執行pure Pipes。如對象是String、Number、Boolean、Symbol、Date、Array」Function、Object。但如果裡面是一個物件,則pure Pipes會忽略他的更動。
這是因為效能的考量,若為純粹物件的值的更動在偵測上較快,但是在物件上屬性的更改的偵測效能會較差。會建議改使用元件的方式去偵測改變。

但angular還是提供了impure pipes的方式可以偵測物件的改變,但使用上要小心不能因此而拖慢系統速度。
它看起來會是像這樣: