細(xì)說 Angular 2+ 的表單(一):模板驅(qū)動型表單
響應(yīng)式表單
響應(yīng)式表單乍一看還是很像模板驅(qū)動型表單的,但響應(yīng)式表單需要引入一個不同的模塊: ReactiveFormsModule
而不是 FormsModule
import {ReactiveFormsModule} from "@angular/forms";
@NgModule({
// 省略其他
imports: [..., ReactiveFormsModule],
// 省略其他
})
// 省略其他
與模板驅(qū)動型表單的區(qū)別
接下來我們還是利用前面的例子,用響應(yīng)式表單的要求改寫一下:
<form [formGroup]="user" (ngSubmit)="onSubmit(user)">
<label>
<span>電子郵件地址</span>
<input type="text" formControlName="email" placeholder="請輸入您的 email 地址">
</label>
<div *ngIf="user.get('email').hasError('required') && user.get('email').touched" class="error">
email 是必填項
</div>
<div *ngIf="user.get('email').hasError('pattern') && user.get('email').touched" class="error">
email 格式不正確
</div>
<div>
<label>
<span>密碼</span>
<input type="password" formControlName="password" placeholder="請輸入您的密碼">
</label>
<div *ngIf="user.get('password').hasError('required') && user.get('password').touched" class="error">
密碼是必填項
</div>
<label>
<span>確認(rèn)密碼</span>
<input type="password" formControlName="repeat" placeholder="請再次輸入密碼">
</label>
<div *ngIf="user.get('repeat').hasError('required') && user.get('repeat').touched" class="error">
確認(rèn)密碼是必填項
</div>
<div *ngIf="user.hasError('validateEqual') && user.get('repeat').touched" class="error">
確認(rèn)密碼和密碼不一致
</div>
</div>
<div formGroupName="address">
<label>
<span>省份</span>
<select formControlName="province">
<option value="">請選擇省份</option>
<option [value]="province" *ngFor="let province of provinces">{{province}}</option>
</select>
</label>
<label>
<span>城市</span>
<select formControlName="city">
<option value="">請選擇城市</option>
<option [value]="city" *ngFor="let city of (cities$ | async)">{{city}}</option>
</select>
</label>
<label>
<span>區(qū)縣</span>
<select formControlName="area">
<option value="">請選擇區(qū)縣</option>
<option [value]="area" *ngFor="let area of (areas$ | async)">{{area}}</option>
</select>
</label>
<label>
<span>地址</span>
<input type="text" formControlName="addr">
</label>
</div>
<button type="submit" [disabled]="user.invalid">注冊</button>
</form>
這段代碼和模板驅(qū)動型表單的那段看起來差不多,但是有幾個區(qū)別:
- 表單多了一個指令
[formGroup]="user"
- 去掉了對表單的引用
#f="ngForm"
- 每個控件多了一個
formControlName
- 但同時每個控件也去掉了驗證條件,比如
required
、minlength
等 - 在地址分組中用
formGroupName="address"
替代了ngModelGroup="address"
模板上的區(qū)別大概就這樣了,接下來我們來看看組件的區(qū)別:
import { Component, OnInit } from '@angular/core';
import { FormControl, FormGroup, Validators } from "@angular/forms";
@Component({
selector: 'app-model-driven',
templateUrl: './model-driven.component.html',
styleUrls: ['./model-driven.component.css']
})
export class ModelDrivenComponent implements OnInit {
user: FormGroup;
ngOnInit() {
// 初始化表單
this.user = new FormGroup({
email: new FormControl('', [Validators.required, Validators.pattern(/([a-zA-Z0-9]+[_|_|.]?)*[a-zA-Z0-9]+@([a-zA-Z0-9]+[_|_|.]?)*[a-zA-Z0-9]+.[a-zA-Z]{2,4}/)]),
password: new FormControl('', [Validators.required]),
repeat: new FormControl('', [Validators.required]),
address: new FormGroup({
province: new FormControl(''),
city: new FormControl(''),
area: new FormControl(''),
addr: new FormControl('')
})
});
}
onSubmit({value, valid}){
if(!valid) return;
console.log(JSON.stringify(value));
}
}
從上面的代碼中我們可以看到,這里的表單( FormGroup
)是由一系列的表單控件( FormControl
)構(gòu)成的。其實 FormGroup
的構(gòu)造函數(shù)接受的是三個參數(shù): controls
(表單控件『數(shù)組』,其實不是數(shù)組,是一個類似字典的對象) 、 validator
(驗證器) 和 asyncValidator
(異步驗證器) ,其中只有 controls
數(shù)組是必須的參數(shù),后兩個都是可選參數(shù)。
// FormGroup 的構(gòu)造函數(shù)
constructor(
controls: {
[key: string]: AbstractControl;
},
validator?: ValidatorFn,
asyncValidator?: AsyncValidatorFn
)
我們上面的代碼中就沒有使用驗證器和異步驗證器的可選參數(shù),而且注意到我們提供 controls
的方式是,一個 key
對應(yīng)一個 FormControl
。比如下面的 key
是 password
,對應(yīng)的值是 new FormControl('', [Validators.required])
。這個 key
對應(yīng)的就是模板中的 formControlName
的值,我們模板代碼中設(shè)置了 formControlName="password"
,而表單控件會根據(jù)這個 password
的控件名來跟蹤實際的渲染出的表單頁面上的控件(比如 <input formcontrolname="password">
)的值和驗證狀態(tài)。
password: new FormControl('', [Validators.required])
那么可以看出,這個表單控件的構(gòu)造函數(shù)同樣也接受三個可選參數(shù),分別是:控件初始值( formState
)、控件驗證器或驗證器數(shù)組( validator
)和控件異步驗證器或異步驗證器數(shù)組( asyncValidator
)。上面的那行代碼中,初始值為空字符串,驗證器是『必選』,而異步驗證器我們沒有提供。
// FormControl 的構(gòu)造函數(shù)
constructor(
formState?: any, // 控件初始值
validator?: ValidatorFn | ValidatorFn[], // 控件驗證器或驗證器數(shù)組
asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] // 控件異步驗證器或異步驗證器數(shù)組
)
由此可以看出,響應(yīng)式表單區(qū)別于模板驅(qū)動型表單的的主要特點在于:是由組件類去創(chuàng)建、維護和跟蹤表單的變化,而不是依賴模板。
那么我們是否在響應(yīng)式表單中還可以使用 ngModel
呢?當(dāng)然可以,但這樣的話表單的值會在兩個不同的位置存儲了: ngModel
綁定的對象和 FormGroup
,這個在設(shè)計上我們一般是要避免的,也就是說盡管可以這么做,但我們不建議這么做。
FormBuilder 快速構(gòu)建表單
上面的表單構(gòu)造起來雖然也不算太麻煩,但是在表單項目逐漸多起來之后還是一個挺麻煩的工作,所以 Angular 提供了一種快捷構(gòu)造表單的方式 -- 使用 FormBuilder。
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from "@angular/forms";
@Component({
selector: 'app-model-driven',
templateUrl: './model-driven.component.html',
styleUrls: ['./model-driven.component.css']
})
export class ModelDrivenComponent implements OnInit {
user: FormGroup;
constructor(private fb: FormBuilder) {
}
ngOnInit() {
// 初始化表單
this.user = this.fb.group({
email: ['', [Validators.required, Validators.email]],
password: ['', Validators.required],
repeat: ['', Validators.required],
address: this.fb.group({
province: [],
city: [],
area: [],
addr: []
})
});
}
// 省略其他部分
}
使用 FormBuilder 我們可以無需顯式聲明 FormControl 或 FormGroup 。 FormBuilder 提供三種類型的快速構(gòu)造: control
, group
和 array
,分別對應(yīng) FormControl, FormGroup 和 FormArray。 我們在表單中最常見的一種是通過 group
來初始化整個表單。上面的例子中,我們可以看到 group
接受一個字典對象作為參數(shù),這個字典中的 key 就是這個 FormGroup 中 FormControl 的名字,值是一個數(shù)組,數(shù)組中的第一個值是控件的初始值,第二個是同步驗證器的數(shù)組,第三個是異步驗證器數(shù)組(第三個并未出現(xiàn)在我們的例子中)。這其實已經(jīng)在隱性的使用 FormBuilder.control
了,可以參看下面的 FormBuilder 中的 control
函數(shù)定義,其實 FormBuilder 利用我們給出的值構(gòu)造了相對應(yīng)的 control
:
control(
formState: Object,
validator?: ValidatorFn | ValidatorFn[],
asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[]
): FormControl;
此外還值得注意的一點是 address 的處理,我們可以清晰的看到 FormBuilder 支持嵌套,遇到 FormGroup 時僅僅需要再次使用 this.fb.group({...})
即可。這樣我們的表單在擁有大量的表單項時,構(gòu)造起來就方便多了。
自定義驗證
對于響應(yīng)式表單來說,構(gòu)造一個自定義驗證器是非常簡單的,比如我們上面提到過的的驗證 密碼
和 重復(fù)輸入密碼
是否相同的需求,我們在響應(yīng)式表單中來試一下。
validateEqual(passwordKey: string, confirmPasswordKey: string): ValidatorFn {
return (group: FormGroup): {[key: string]: any} => {
const password = group.controls[passwordKey];
const confirmPassword = group.controls[confirmPasswordKey];
if (password.value !== confirmPassword.value) {
return { validateEqual: true };
}
return null;
}
}
這個函數(shù)的邏輯比較簡單:我們接受兩個字符串(是 FormControl 的名字),然后返回一個 ValidatorFn
。但是這個函數(shù)里面就奇奇怪怪的,
比如 (group: FormGroup): {[key: string]: any} => {...}
是什么意思???還有,這個 ValidatorFn
是什么鬼?我們來看一下定義:
export interface ValidatorFn {
(c: AbstractControl): ValidationErrors | null;
}
這樣就清楚了, ValidatorFn
是一個對象定義,這個對象中有一個方法,此方法接受一個 AbstractControl
類型的參數(shù)(其實也就是我們的 FormControl,而 AbstractControl 為其父類),而這個方法還要返回 ValidationErrors
,這個 ValidationErrors
的定義如下:
export declare type ValidationErrors = {
[key: string]: any;
};
回過頭來再看我們的這句 (group: FormGroup): {[key: string]: any} => {...}
,大家就應(yīng)該明白為什么這么寫了,我們其實就是在返回一個 ValidatorFn
類型的對象。只不過我們利用 javascript/typescript
對象展開的特性把 ValidationErrors
寫成了 {[key: string]: any}
。
弄清楚這個函數(shù)的邏輯后,我們怎么使用呢?非常簡單,先看代碼:
this.user = this.fb.group({
email: ['', [Validators.required, Validators.email]],
password: ['', Validators.required],
repeat: ['', Validators.required],
address: this.fb.group({
province: [],
city: [],
area: [],
addr: []
})
}, {validator: this.validateEqual('password', 'repeat')});
和最初的代碼相比,多了一個參數(shù),那就是 {validator: this.validateEqual('password', 'repeat')}
。FormBuilder 的 group
函數(shù)接受兩個參數(shù),第一個就是那串長長的,我們叫它 controlsConfig
,用于表單控件的構(gòu)造,以及每個表單控件的驗證器。但是如果一個驗證器是要計算多個 field
的話,我們可以把它作為整個 group
的驗證器。所以 FormBuilder 的 group
函數(shù)還接收第二個參數(shù),這個參數(shù)中可以提供同步驗證器或異步驗證器。同樣還是一個字典對象,是同步驗證器的話,key
寫成 validator,異步的話寫成 asyncValidator
。
現(xiàn)在我們可以保存代碼,啟動 ng serve
到瀏覽器中看一下結(jié)果了:

FormArray 有什么用?
我們在購物網(wǎng)站經(jīng)常遇到需要維護多個地址,因為我們有些商品希望送到公司,有些需要送到家里,還有些給父母采購的需要送到父母那里。這就是一個典型的 FormArray 可以派上用場的場景。所有的這些地址的結(jié)構(gòu)都是一樣的,有省、市、區(qū)縣和街道地址,那么對于處理這樣的場景,我們來看看在響應(yīng)式表單中怎么做。
首先,我們需要把 HTML 模板改造一下,現(xiàn)在的地址是多項了,所以我們需要在原來的地址部分外面再套一層,并且聲明成 formArrayName="addrs"
。 FormArray 顧名思義是一個數(shù)組,所以我們要對這個控件數(shù)組做一個循環(huán),然后讓每個數(shù)組元素是 FormGroup,只不過這次我們的 [formGroupName]="i"
是讓 formGroupName
等于該數(shù)組元素的索引。
<div formArrayName="addrs">
<button (click)="addAddr()">Add</button>
<div *ngFor="let item of user.controls['addrs'].controls; let i = index;">
<div [formGroupName]="i">
<label>
<span>省份</span>
<select formControlName="province">
<option value="">請選擇省份</option>
<option [value]="province" *ngFor="let province of provinces">{{province}}</option>
</select>
</label>
<label>
<span>城市</span>
<select formControlName="city">
<option value="">請選擇城市</option>
<option [value]="city" *ngFor="let city of (cities$ | async)">{{city}}</option>
</select>
</label>
<label>
<span>區(qū)縣</span>
<select formControlName="area">
<option value="">請選擇區(qū)縣</option>
<option [value]="area" *ngFor="let area of (areas$ | async)">{{area}}</option>
</select>
</label>
<label>
<span>地址</span>
<input type="text" formControlName="street">
</label>
</div>
</div>
</div>
改造好模板后,我們需要在類文件中也做對應(yīng)處理,去掉原來的 address: this.fb.group({...})
,換成 addrs: this.fb.array([])
。而
this.user = this.fb.group({
email: ['', [Validators.required, Validators.email]],
password: ['', Validators.required],
repeat: ['', Validators.required],
addrs: this.fb.array([])
}, {validator: this.validateEqual('password', 'repeat')});
但這樣我們是看不到也增加不了新的地址的,因為我們還沒有處理添加的邏輯呢,下面我們就添加一下:其實就是建立一個新的 FormGroup,然后加入 FormArray 數(shù)組中。
addAddr(): void {
(<FormArray>this.user.controls['addrs']).push(this.createAddrItem());
}
private createAddrItem(): FormGroup {
return this.fb.group({
province: [],
city: [],
area: [],
street: []
})
}
到這里我們的結(jié)構(gòu)就建好了,保存后,到瀏覽器中去試試添加多個地址吧!

響應(yīng)式表單的優(yōu)勢
首先是可測試能力。模板驅(qū)動型表單進行單元測試是比較困難的,因為驗證邏輯是寫在模板中的。但驗證器的邏輯單元測試對于響應(yīng)式表單來說就非常簡單了,因為你的驗證器無非就是一個函數(shù)而已。
當(dāng)然除了這個優(yōu)點,我們對表單可以有完全的掌控:從初始化表單控件的值、更新和獲取表單值的變化到表單的驗證和提交,這一系列的流程都在程序邏輯控制之下。
而且更重要的是,我們可以使用函數(shù)響應(yīng)式編程的風(fēng)格來處理各種表單操作,因為響應(yīng)式表單提供了一系列支持 Observable
的接口 API 。那么這又能說明什么呢?有什么用呢?
首先是無論表單本身還是控件都可以看成是一系列的基于時間維度的數(shù)據(jù)流了,這個數(shù)據(jù)流可以被多個觀察者訂閱和處理,由于 valueChanges
本身是個 Observable
,所以我們就可以利用 RxJS 提供的豐富的操作符,將一個對數(shù)據(jù)驗證、處理等的完整邏輯清晰的表達出來。當(dāng)然現(xiàn)在我們不會對 RxJS 做深入的討論,后面有專門針對 RxJS 進行講解的章節(jié)。
this.form.valueChanges
.filter((value) => this.user.valid)
.subscribe((value) => {
console.log("現(xiàn)在時刻表單的值為 ",JSON.stringify(value));
});
上面的例子中,我們?nèi)〉帽韱沃档淖兓?,然后過濾掉表單存在非法值的情況,然后輸出表單的值。這只是非常簡單的一個 Rx 應(yīng)用,隨著邏輯復(fù)雜度的增加,我們后面會見證 Rx 卓越的處理能力。
慕課網(wǎng) Angular 視頻課上線: http://coding.imooc.com/class/123.html?mc_marking=1fdb7649e8a8143e8b81e221f9621c4a&mc_channel=banner