1. 程式人生 > >Angular2表單模型驅動的表單(Model-Driven Forms)

Angular2表單模型驅動的表單(Model-Driven Forms)

在上一篇Angular2表單-模板驅動的表單文章中,介紹了模板驅動的表單,這裡就不再贅述。雖然模板驅動的表單使用起來很方便,但是,當你的表單變得越來越複雜,特別是控制元件之間存在很多資料的互動,例如很常見的購物車,購物車裡面會有很多商品,如果是類似淘寶這樣的網站,這些商品還需要按照店鋪分組;每個商品有單價和數量,每個店鋪甚至每個商品可能有一些優惠券可以使用,甚至會有淘寶平臺的減滿券;當每個商品的單價或數量改變的時候,每個店鋪的商品總金額、和總金額都會發生改變。像這種複雜的表單,資料之間的互動非常多,對開發和測試都會非常不方便。如果使用模板驅動的表單,測試是基於瀏覽器的端對端測試,測試用例也很不好寫。對於這種情況,使用Angular2的另一種表單,也就是模型驅動的表單(Model-Driven Forms)會更加方便。

但是,需要說明的是,使用模型驅動的表單,並不是說就不需要寫頁面,頁面模板上的表單也不會自動生成。只是說,我們不需要像模板驅動的表單一樣,在頁面上設定model、驗證器等。

例項

下圖是這篇文章使用的例項的介面,跟上一篇介紹模板驅動的表單使用的例項一樣:
screen.png

專案原始碼可以從github獲取,這個專案包含了幾個Angular2表單相關的例項,可以使用下面的命令獲取本文所對應的程式碼:

1 git clone https://github.com/Mavlarn/angular2-forms-tutorial

然後進入專案目錄,執行下面的命令安裝依賴然後執行測試伺服器:

1234 cd angular2-forms-tutorialgit checkout model-driven # 檢出該文所使用的tagnpm installnpm start

form

我們還是用上一篇文章使用的例項,來建立一個簡單的修改個人資訊的表單,表單頁面如下:

1234567891011121314 <form> <label>姓名:</label> <input type="text"> <label>電話:</label> <input type="text"> <fieldset> <label>城市:</label> <input type="text"> <label
>
街道:</label> <input type="text"> </fieldset> <button type="submit">儲存</button></form>

同樣,為了節省頁面篇幅,省略了很多樣式,在例項中可以看到使用的bootstrap的樣式,例如姓名欄位實際上是這樣:

123456 <div class="form-group"> <label class="col-sm-2 control-label">姓名:</label> <div class="col-sm-10"> <input class="form-control" type="text"> </div></div>

元件

然後,我們的元件是這樣:

123456789101112131415161718 import { Component } from '@angular/core';import { FormGroup, FormControl } from '@angular/forms';@Component({ selector: 'reactive-form', templateUrl: 'app/reactive-forms/reactive-forms.component.html', styleUrls: ['app/reactive-forms/reactive-forms.component.css']})export class ReactiveFormsComponent { userForm = new FormGroup({ name: new FormControl(), mobile: new FormControl(), address: new FormGroup({ city: new FormControl(), street: new FormControl() }) });}

我們在元件裡面,建立了一個FormGroup型別的物件,他就是對應的頁面上的表單資料,其中,address也是一個group,裡面又有2個屬性。

繫結元件和表單元素

我們在元件中手動建立了這個表單控制元件組,裡面包含所有的元件,對應頁面上的表單元素。但是,我們需要把這個元件中的資料繫結到頁面上。我們除了用Angular2的資料繫結的方式繫結這個資料到模板上以外,我們還需要針對表單控制元件做對映:

1234567891011121314 <form [formGroup]="userForm"> <label>姓名:</label> <input type="text" formControlName="name"> <label>電話:</label> <input type="text" formControlName="mobile"> <fieldset formGroupName="address"> <label>城市:</label> <input type="text" formControlName="city"> <label>街道:</label> <input type="text" formControlName="street"> </fieldset> <button type="submit">儲存</button></form>

首先我們用[formGroup]="userForm"將頁面上的表單的formGroup(單向)繫結到元件中的userForm變數上。
然後,通過<input type="text" formControlName="name">,將裡面的表單的輸入元件繫結到userForm裡面的name控制元件上。在上面的ReactiveFormsComponent元件中,我們建立了userForm控制元件組,裡面有一個FormControl,叫name
使用這種繫結方式,我們把頁面上的表單元素和元件中程式碼建立的表單控制元件關聯起來。

簡化表單控制元件建立

在上面的ReactiveFormsComponent元件中我們建立表單的方式其實也可以使用FormBuilder簡化。同時還可以在新建的FormControl的時候可以設定初始值已經驗證器等,具體看下面的程式碼:

12345678910111213141516 import { Component, OnInit } from '@angular/core';import { FormGroup, FormControl, FormBuilder, Validators } from '@angular/forms';export class ReactiveFormsComponent implements OnInit { userForm: FormGroup; constructor(private formBuilder: FormBuilder) {} ngOnInit() { this.userForm = this.formBuilder.group({ name: ['張三', [Validators.required, Validators.minLength(3)]], mobile: [13800138001, [Validators.required, Validators.minLength(11), Validators.maxLength(11)]], address: this.formBuilder.group({ city: ['北京', Validators.required], street: ['朝陽望京...', Validators.required] }) }); }}

在這個控制元件中,姓名控制元件設定了初始值’張三’和2個驗證器(Validators.required, Validators.minLength(3)),手機號的控制元件也設定了初始值和3個驗證器。

頁面上顯示控制元件狀態

最後,跟’模板驅動的表單’類似,我們還可以在頁面上新增驗證器和表單控制元件的狀態。只不過,在’模板驅動的表單’中,我們可以使用模板引用變數#name="ngModel"來建立一個針對姓名控制元件的引用變數。但是在這裡,我們沒有一個ngModel在模板裡,所以就不能用這種方式,我們只能使用表單控制元件的引用來獲得所有控制元件的狀態,例如userForm.controls.name.dirty這樣。下面就是針對姓名控制元件,新增的根據狀態顯示各種資訊的例項:

12345678 <input type="text" formControlName="name"><span *ngIf="userForm.controls.name.pristine">未修改</span><span *ngIf="userForm.controls.name.dirty">已修改</span><span *ngIf="userForm.controls.name.valid">有效</span><div [hidden]="userForm.controls.name.valid||userForm.controls.name.pristine"> <p *ngIf="userForm.controls.name.errors?.minlength">姓名最小長度為3</p> <p *ngIf="userForm.controls.name.errors?.required">必須輸入姓名</p></div>

當然,跟模板驅動的表單一樣,這種表單也會根據狀態在html元素上新增各種class。當一個控制元件的值通過驗證器驗證有效以後,在這個html元素上就會新增一個ng-valid的class;如果驗證失敗,就會新增一個ng-invalid。我們就可以使用css樣式來顯示不同的狀態:

123456789 .ng-valid[required], .ng-valid.required { border-left: 5px solid #42A948; /* green */}.ng-invalid:not(form).ng-invalid:not(fieldset) { border-left: 5px solid #a94442; /* red */}.error-msg { color: red;}

在上面的定義中,我們對驗證型別為required、狀態為valid的控制元件添加了一個左邊綠色顯示的樣式。但是,實際上這一個樣式不會起作用,因為我們的驗證器不是在頁面模版中新增的,例如在’姓名’是這樣:

1 <input type="text" formControlName="name">

我們沒有新增一個required的屬性給他,所以ng-valid[required]這個選擇器不會起作用。

響應式處理表單資料

模型驅動表單,之所以又叫響應式表單(Reactive Forms),最重要的原因就是我們可以使用響應式程式設計來處理表單中的資料。響應式表單的控制元件提供一個Observables型別的資料更新物件,我們可以使用Observables的各種特性來處理表單中資料的改變。
有關Observables的特性和如何利用它的特性處理表單資料,可以閱讀這篇文章《利用Angular2的Observables實現互動控制》
在這裡,我們直接使用它來處理資料更新:

1234567891011121314151617181920212223242526272829303132333435 export class ReactiveFormsComponent implements OnInit { userForm: FormGroup; msg: String; changeMsg: any; constructor(private formBuilder: FormBuilder) {} ngOnInit() { this.userForm = this.formBuilder.group({ name: ['張三', [Validators.required, Validators.minLength(3)]], mobile: [13800138001, [Validators.required, Validators.minLength(11), Validators.maxLength(11)]], address: this.formBuilder.group({ city: ['北京', Validators.required], street: ['朝陽望京...', Validators.required] }) }); // 從表單控制元件中獲得地址、城市、街道控制元件的引用 // 地址控制元件也是一個FormGroup,需要將它轉型成FormGroup型別。 const addr$ = <FormGroup>this.userForm.controls['address']; const city$ = addr$.controls['city']; const street$ = addr$.controls['street']; city$.valueChanges.debounceTime(1000).distinctUntilChanged().subscribe(cityValue => { this.msg = cityValue + ' 歡迎你!'; street$.setValue(cityValue); }); this.userForm.valueChanges.subscribe(x => this.changeMsg = { event: 'Form DATA CHANGED', object: x }); } reset() { // 我們同樣可以使用reset方法來重置資料 this.userForm.reset(); } }

上面的debounceTime(1000)是指當用戶修改輸入的值以後,過1000毫秒才會觸發後面的處理方法。
distinctUntilChanged()是指如果使用者如果在1000毫秒內,輸入了一個字元,又刪掉了,那麼輸入的值應該沒有改變,這種情況下,後面的處理方法就不會被觸發。
subscribe()就是註冊一個處理方法,就是當有資料發生改變的時候,需要出發的方法。在這個方法裡,當用戶輸入了一個新的城市名以後,就會更新街道的值也為這個城市名,同時下面的顯示資訊也相應改變。

實際上,對於模板驅動的表單的資料更新,我們也可以使用這種響應式的數理方法。只是在之前的例項中,沒有使用這種處理方式而已。

引入Observables的操作符

在新增上面的debounceTime(1000)方法以後,實際上編輯器會報編譯錯誤,是因為我們還需要手動的引入相關的操作符。我們可以把這個引入的定義新增到這個ReactiveFormsComponent所在的檔案中,也可以在這個元件的任意父元件中引入。區別就是,在父元件中引入的話,那麼在其他元件中也能夠使用。所以,我們就在app.module.ts中引入,這樣在所有的元件中都可以使用這些方法。

12 import 'rxjs/add/operator/debounceTime';import 'rxjs/add/operator/distinctUntilChanged';

為什麼我們需要一個一個方法的引入呢?因為這樣,我們就只需要引入我們用到的方法,不需要的部分就不會被引入。這樣在最後打包的時候,不需要的部分不會被打包,就能夠減少程式碼量。

模板驅動和模型驅動表單的區別

看到這裡,其實就會發現,這兩種表單建立方式,主要有2個區別:

  1. 資料模型、驗證器的定義
    使用模型驅動的表單,我們的頁面可以很整潔,沒有驗證器,沒有資料繫結,沒有onChange, onBlur等事件繫結。我們可以在元件程式碼裡訂閱資料更新的事件,實現資料之間的互動。
  2. 是否可以單元測試
    目前我們還沒有使用任何測試方法,但是,在Angular的最佳實踐裡,測試是很重要的,它包括2中測試,單元測試以及端到端測試,單元測試可以不依賴瀏覽器,而端到端測試的執行必須在瀏覽器裡執行。而模型驅動的表單,由於他的資料模型、驗證器、互動等都是在元件中,我們可以完全不依賴瀏覽器就在單元測試裡面測試表單的各種邏輯。

所以,當你有一個表單,考慮用哪種表單建立方式的時候,如果這個表單比較簡單,甚至不需要測試,那就用模板驅動的表單就可以。如果表單比較複雜,就應該考慮使用模型驅動的表單方式。