用表單處理用戶輸入是許多常見應(yīng)用的基礎(chǔ)功能。 應(yīng)用通過表單來讓用戶登錄、修改個人檔案、輸入敏感信息以及執(zhí)行各種數(shù)據(jù)輸入任務(wù)。
Angular 提供了兩種不同的方法來通過表單處理用戶輸入:響應(yīng)式表單和模板驅(qū)動表單。 兩者都從視圖中捕獲用戶輸入事件、驗證用戶輸入、創(chuàng)建表單模型、修改數(shù)據(jù)模型,并提供跟蹤這些更改的途徑。
本指南提供的信息可以幫你確定哪種方式最適合你的情況。它介紹了這兩種方法所用的公共構(gòu)造塊,還總結(jié)了兩種方式之間的關(guān)鍵區(qū)別,并在建立、數(shù)據(jù)流和測試等不同的情境下展示了這些差異。
響應(yīng)式表單和模板驅(qū)動表單以不同的方式處理和管理表單數(shù)據(jù)。每種方法都有各自的優(yōu)點。
表單 |
詳情 |
---|---|
響應(yīng)式表單 |
提供對底層表單對象模型直接、顯式的訪問。它們與模板驅(qū)動表單相比,更加健壯:它們的可擴展性、可復用性和可測試性都更高。如果表單是你的應(yīng)用程序的關(guān)鍵部分,或者你已經(jīng)在使用響應(yīng)式表單來構(gòu)建應(yīng)用,那就使用響應(yīng)式表單。 |
模板驅(qū)動表單 |
依賴模板中的指令來創(chuàng)建和操作底層的對象模型。它們對于向應(yīng)用添加一個簡單的表單非常有用,比如電子郵件列表注冊表單。它們很容易添加到應(yīng)用中,但在擴展性方面不如響應(yīng)式表單。如果你有可以只在模板中管理的非?;镜谋韱涡枨蠛瓦壿嫞敲茨0弪?qū)動表單就很合適。 |
下表總結(jié)了響應(yīng)式表單和模板驅(qū)動表單之間的一些關(guān)鍵差異。
響應(yīng)式 |
模板驅(qū)動 |
|
---|---|---|
建立表單模型 |
顯式的,在組件類中創(chuàng)建 |
隱式的,由指令創(chuàng)建 |
數(shù)據(jù)模型 |
結(jié)構(gòu)化和不可變的 |
非結(jié)構(gòu)化和可變的 |
數(shù)據(jù)流 |
同步 |
異步 |
表單驗證 |
函數(shù) |
指令 |
如果表單是應(yīng)用程序的核心部分,那么可伸縮性就非常重要。能夠跨組件復用表單模型是至關(guān)重要的。
響應(yīng)式表單比模板驅(qū)動表單更有可伸縮性。它們提供對底層表單 API 的直接訪問,并且在視圖和數(shù)據(jù)模型之間使用同步數(shù)據(jù)流,從而可以更輕松地創(chuàng)建大型表單。響應(yīng)式表單需要較少的測試設(shè)置,測試時不需要深入理解變更檢測,就能正確測試表單更新和驗證。
模板驅(qū)動表單專注于簡單的場景,可復用性沒那么高。它們抽象出了底層表單 API,并且在視圖和數(shù)據(jù)模型之間使用異步數(shù)據(jù)流。對模板驅(qū)動表單的這種抽象也會影響測試。測試程序非常依賴于手動觸發(fā)變更檢測才能正常運行,并且需要進行更多設(shè)置工作。
響應(yīng)式表單和模板驅(qū)動型表單都會跟蹤用戶與之交互的表單輸入元素和組件模型中的表單數(shù)據(jù)之間的值變更。這兩種方法共享同一套底層構(gòu)建塊,只在如何創(chuàng)建和管理常用表單控件實例方面有所不同。
響應(yīng)式表單和模板驅(qū)動表單都建立在下列基礎(chǔ)類之上。
基類 |
詳情 |
---|---|
FormControl
|
追蹤單個表單控件的值和驗證狀態(tài)。 |
FormGroup
|
追蹤一個表單控件組的值和狀態(tài)。 |
FormArray
|
追蹤表單控件數(shù)組的值和狀態(tài)。 |
ControlValueAccessor
|
在 Angular 的 |
對于響應(yīng)式表單,你可以直接在組件類中定義表單模型。?[formControl]
? 指令會通過內(nèi)部值訪問器來把顯式創(chuàng)建的 ?FormControl
?實例與視圖中的特定表單元素聯(lián)系起來。
下面的組件使用響應(yīng)式表單為單個控件實現(xiàn)了一個輸入字段。在這個例子中,表單模型是 ?FormControl
?實例。
import { Component } from '@angular/core';
import { FormControl } from '@angular/forms';
@Component({
selector: 'app-reactive-favorite-color',
template: `
Favorite Color: <input type="text" [formControl]="favoriteColorControl">
`
})
export class FavoriteColorComponent {
favoriteColorControl = new FormControl('');
}
圖 1 展示了在響應(yīng)式表單中,表單模型是如何成為事實之源(source of truth)的。它通過輸入元素上的 ?[formControl]
? 指令,在任何給定的時間點提供表單元素的值和狀態(tài)。
圖 1. 在響應(yīng)式表單中直接訪問表單模型
在模板驅(qū)動表單中,表單模型是隱式的,而不是顯式的。指令 ?NgModel
?為指定的表單元素創(chuàng)建并管理一個 ?FormControl
?實例。
下面的組件使用模板驅(qū)動表單為單個控件實現(xiàn)了同樣的輸入字段。
import { Component } from '@angular/core';
@Component({
selector: 'app-template-favorite-color',
template: `
Favorite Color: <input type="text" [(ngModel)]="favoriteColor">
`
})
export class FavoriteColorComponent {
favoriteColor = '';
}
在模板驅(qū)動表單中,其事實之源就是模板。你沒有對 ?FormControl
?實例的直接編程訪問,如圖 2 所示。
圖 2. 模板驅(qū)動表單中對表單模型的間接訪問
當應(yīng)用包含一個表單時,Angular 必須讓該視圖與組件模型保持同步,并讓組件模型與視圖保持同步。當用戶通過視圖更改值并進行選擇時,新值必須反映在數(shù)據(jù)模型中。同樣,當程序邏輯改變數(shù)據(jù)模型中的值時,這些值也必須反映到視圖中。
響應(yīng)式表單和模板驅(qū)動表單在處理來自用戶或程序化變更時的數(shù)據(jù)處理方式上有所不同。下面的這些原理圖會以上面定義的 ?favorite-color
? 輸入字段為例,分別說明兩種表單各自的數(shù)據(jù)流。
在響應(yīng)式表單中,視圖中的每個表單元素都直接鏈接到一個表單模型(?FormControl
?實例)。 從視圖到模型的修改以及從模型到視圖的修改都是同步的,而且不依賴于 UI 的渲染方式。
這個視圖到模型的圖表展示了當輸入字段的值發(fā)生變化時,數(shù)據(jù)流是如何從視圖開始經(jīng)過下列步驟進行流動的。
ControlValueAccessor
?會監(jiān)聽表單輸入框元素上的事件,并立即把新值傳給 ?FormControl
?實例。FormControl
?實例會通過 ?valueChanges
?這個可觀察對象發(fā)出這個新值。valueChanges
?的任何一個訂閱者都會收到這個新值。
這個模型到視圖的示意圖體現(xiàn)了程序中對模型的修改是如何通過下列步驟傳播到視圖中的。
favoriteColorControl.setValue()
? 方法被調(diào)用,它會更新這個 ?FormControl
?的值。FormControl
?實例會通過 ?valueChanges
?這個可觀察對象發(fā)出新值。valueChanges
?的任何訂閱者都會收到這個新值。
在模板驅(qū)動表單中,每一個表單元素都是和一個負責管理內(nèi)部表單模型的指令關(guān)聯(lián)起來的。
這個視圖到模型的圖表展示了當輸入字段的值發(fā)生變化時,數(shù)據(jù)流是如何從視圖開始經(jīng)過下列步驟進行流動的。
FormControl
?實例上的 ?setValue()
? 方法。FormControl
?實例通過 ?valueChanges
?這個可觀察對象發(fā)出新值。valueChanges
?的任何訂閱者都會收到新值。ControlValueAccessory
?還會調(diào)用 ?NgModel.viewToModelUpdate()
? 方法,它會發(fā)出一個 ?ngModelChange
?事件。favoriteColor
?,組件中的 ?favoriteColor
?屬性就會修改為 ?ngModelChange
?事件所發(fā)出的值("Blue")。
這個模型到視圖的示意圖展示了當 ?favoriteColor
?從藍變到紅時,數(shù)據(jù)是如何經(jīng)過如下步驟從模型流動到視圖的。
favoriteColor
?的值。NgModel
?指令上的 ?ngOnChanges
?生命周期鉤子。ngOnChanges()
? 方法會把一個異步任務(wù)排入隊列,以設(shè)置內(nèi)部 ?FormControl
?實例的值。FormControl
?實例賦值的任務(wù)就會執(zhí)行。FormControl
?實例通過可觀察對象 ?valueChanges
?發(fā)出最新值。valueChanges
?的任何訂閱者都會收到這個新值。ControlValueAccessor
?會使用 ?favoriteColor
?的最新值來修改表單的輸入框元素。
變更追蹤的方法對應(yīng)用的效率有著重要影響。
表格 |
詳細信息 |
---|---|
響應(yīng)式表單 |
通過以不可變的數(shù)據(jù)結(jié)構(gòu)提供數(shù)據(jù)模型,來保持數(shù)據(jù)模型的純粹性。每當在數(shù)據(jù)模型上觸發(fā)更改時, |
模板驅(qū)動表單 |
依賴于可變性和雙向數(shù)據(jù)綁定,可以在模板中做出更改時更新組件中的數(shù)據(jù)模型。由于使用雙向數(shù)據(jù)綁定時沒有用來對數(shù)據(jù)模型進行跟蹤的唯一性更改,因此變更檢測在需要確定何時更新時效率較低。 |
前面那些使用 ?favorite-color
? 輸入元素的例子就演示了這種差異。
FormControl
?的實例總會返回一個新值favorite-color
? 屬性總會被修改為新值驗證是管理任何表單時必備的一部分。無論你是要檢查必填項,還是查詢外部 API 來檢查用戶名是否已存在,Angular 都會提供一組內(nèi)置的驗證器,以及創(chuàng)建自定義驗證器所需的能力。
表格 |
詳細信息 |
---|---|
響應(yīng)式表單 |
把自定義驗證器定義成函數(shù),它以要驗證的控件作為參數(shù) |
模板驅(qū)動表單 |
和模板指令緊密相關(guān),并且必須提供包裝了驗證函數(shù)的自定義驗證器指令 |
測試在復雜的應(yīng)用程序中也起著重要的作用。當驗證你的表單功能是否正確時,更簡單的測試策略往往也更有用。測試響應(yīng)式表單和模板驅(qū)動表單的差別之一在于它們是否需要渲染 UI 才能基于表單控件和表單字段變化來執(zhí)行斷言。下面的例子演示了使用響應(yīng)式表單和模板驅(qū)動表單時表單的測試過程。
響應(yīng)式表單提供了相對簡單的測試策略,因為它們能提供對表單和數(shù)據(jù)模型的同步訪問,而且不必渲染 UI 就能測試它們。在這些測試中,控件和數(shù)據(jù)是通過控件進行查詢和操縱的,不需要和變更檢測周期打交道。
下面的測試利用前面例子中的 "喜歡的顏色" 組件來驗證響應(yīng)式表單中的 "從視圖到模型" 和 "從模型到視圖" 數(shù)據(jù)流。
驗證“從視圖到模型”的數(shù)據(jù)流
第一個例子執(zhí)行了下列步驟來驗證“從視圖到模型”數(shù)據(jù)流。
favoriteColorControl
?的值與來自輸入框的值是匹配的。it('should update the value of the input field', () => {
const input = fixture.nativeElement.querySelector('input');
const event = createNewEvent('input');
input.value = 'Red';
input.dispatchEvent(event);
expect(fixture.componentInstance.favoriteColorControl.value).toEqual('Red');
});
下一個例子執(zhí)行了下列步驟來驗證“從模型到視圖”數(shù)據(jù)流。
favoriteColorControl
?這個 ?FormControl
?實例來設(shè)置新值。it('should update the value in the control', () => {
component.favoriteColorControl.setValue('Blue');
const input = fixture.nativeElement.querySelector('input');
expect(input.value).toBe('Blue');
});
使用模板驅(qū)動表單編寫測試就需要詳細了解變更檢測過程,以及指令在每個變更檢測周期中如何運行,以確保在正確的時間查詢、測試或更改元素。
下面的測試使用了以前的 "喜歡的顏色" 組件,來驗證模板驅(qū)動表單的 "從視圖到模型" 和 "從模型到視圖" 數(shù)據(jù)流。
下面的測試驗證了 "從視圖到模型" 數(shù)據(jù)流:
it('should update the favorite color in the component', fakeAsync(() => {
const input = fixture.nativeElement.querySelector('input');
const event = createNewEvent('input');
input.value = 'Red';
input.dispatchEvent(event);
fixture.detectChanges();
expect(component.favoriteColor).toEqual('Red');
}));
這個 "視圖到模型" 測試的執(zhí)行步驟如下:
favoriteColor
?屬性的值與來自輸入框的值是匹配的。
下面的測試驗證了 "從模型到視圖" 的數(shù)據(jù)流:
it('should update the favorite color on the input field', fakeAsync(() => {
component.favoriteColor = 'Blue';
fixture.detectChanges();
tick();
const input = fixture.nativeElement.querySelector('input');
expect(input.value).toBe('Blue');
}));
這個 "模型到視圖" 測試的執(zhí)行步驟如下:
favoriteColor
?的值。
fakeAsync()
? 任務(wù)中使用 ?tick()
? 方法來模擬時間的流逝。
favoriteColor
?屬性值是匹配的。
更多建議: