細說 Angular 2+ 的表單(一):模板驅動型表單javascript
響應式表單乍一看仍是很像模板驅動型表單的,但響應式表單須要引入一個不一樣的模塊: ReactiveFormsModule
而不是 FormsModule
css
import {ReactiveFormsModule} from "@angular/forms";
@NgModule({
// 省略其餘
imports: [..., ReactiveFormsModule],
// 省略其餘
})
// 省略其餘複製代碼
接下來咱們仍是利用前面的例子,用響應式表單的要求改寫一下:html
<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>確認密碼</span>
<input type="password" formControlName="repeat" placeholder="請再次輸入密碼">
</label>
<div *ngIf="user.get('repeat').hasError('required') && user.get('repeat').touched" class="error">
確認密碼是必填項
</div>
<div *ngIf="user.hasError('validateEqual') && user.get('repeat').touched" class="error">
確認密碼和密碼不一致
</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>區縣</span>
<select formControlName="area">
<option value="">請選擇區縣</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>複製代碼
這段代碼和模板驅動型表單的那段看起來差很少,可是有幾個區別:java
[formGroup]="user"
#f="ngForm"
formControlName
required
、minlength
等formGroupName="address"
替代了 ngModelGroup="address"
模板上的區別大概就這樣了,接下來咱們來看看組件的區別:typescript
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
)構成的。其實 FormGroup
的構造函數接受的是三個參數: controls
(表單控件『數組』,其實不是數組,是一個相似字典的對象) 、 validator
(驗證器) 和 asyncValidator
(異步驗證器) ,其中只有 controls
數組是必須的參數,後兩個都是可選參數。編程
// FormGroup 的構造函數
constructor(
controls: {
[key: string]: AbstractControl;
},
validator?: ValidatorFn,
asyncValidator?: AsyncValidatorFn
)複製代碼
咱們上面的代碼中就沒有使用驗證器和異步驗證器的可選參數,並且注意到咱們提供 controls
的方式是,一個 key
對應一個 FormControl
。好比下面的 key
是 password
,對應的值是 new FormControl('', [Validators.required])
。這個 key
對應的就是模板中的 formControlName
的值,咱們模板代碼中設置了 formControlName="password"
,而表單控件會根據這個 password
的控件名來跟蹤實際的渲染出的表單頁面上的控件(好比 <input formcontrolname="password">
)的值和驗證狀態。數組
password: new FormControl('', [Validators.required])複製代碼
那麼能夠看出,這個表單控件的構造函數一樣也接受三個可選參數,分別是:控件初始值( formState
)、控件驗證器或驗證器數組( validator
)和控件異步驗證器或異步驗證器數組( asyncValidator
)。上面的那行代碼中,初始值爲空字符串,驗證器是『必選』,而異步驗證器咱們沒有提供。瀏覽器
// FormControl 的構造函數
constructor(
formState?: any, // 控件初始值
validator?: ValidatorFn | ValidatorFn[], // 控件驗證器或驗證器數組
asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] // 控件異步驗證器或異步驗證器數組
)複製代碼
由此能夠看出,響應式表單區別於模板驅動型表單的的主要特色在於:是由組件類去建立、維護和跟蹤表單的變化,而不是依賴模板。app
那麼咱們是否在響應式表單中還可使用 ngModel
呢?固然能夠,但這樣的話表單的值會在兩個不一樣的位置存儲了: ngModel
綁定的對象和 FormGroup
,這個在設計上咱們通常是要避免的,也就是說盡管能夠這麼作,但咱們不建議這麼作。異步
上面的表單構造起來雖然也不算太麻煩,可是在表單項目逐漸多起來以後仍是一個挺麻煩的工做,因此 Angular 提供了一種快捷構造表單的方式 -- 使用 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 提供三種類型的快速構造: control
, group
和 array
,分別對應 FormControl, FormGroup 和 FormArray。 咱們在表單中最多見的一種是經過 group
來初始化整個表單。上面的例子中,咱們能夠看到 group
接受一個字典對象做爲參數,這個字典中的 key 就是這個 FormGroup 中 FormControl 的名字,值是一個數組,數組中的第一個值是控件的初始值,第二個是同步驗證器的數組,第三個是異步驗證器數組(第三個並未出如今咱們的例子中)。這其實已經在隱性的使用 FormBuilder.control
了,能夠參看下面的 FormBuilder 中的 control
函數定義,其實 FormBuilder 利用咱們給出的值構造了相對應的 control
:
control(
formState: Object,
validator?: ValidatorFn | ValidatorFn[],
asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[]
): FormControl;複製代碼
此外還值得注意的一點是 address 的處理,咱們能夠清晰的看到 FormBuilder 支持嵌套,遇到 FormGroup 時僅僅須要再次使用 this.fb.group({...})
便可。這樣咱們的表單在擁有大量的表單項時,構造起來就方便多了。
對於響應式表單來講,構造一個自定義驗證器是很是簡單的,好比咱們上面提到過的的驗證 密碼
和 重複輸入密碼
是否相同的需求,咱們在響應式表單中來試一下。
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;
}
}複製代碼
這個函數的邏輯比較簡單:咱們接受兩個字符串(是 FormControl 的名字),而後返回一個 ValidatorFn
。可是這個函數裏面就奇奇怪怪的,
好比 (group: FormGroup): {[key: string]: any} => {...}
是什麼意思啊?還有,這個 ValidatorFn
是什麼鬼?咱們來看一下定義:
export interface ValidatorFn {
(c: AbstractControl): ValidationErrors | null;
}複製代碼
這樣就清楚了, ValidatorFn
是一個對象定義,這個對象中有一個方法,此方法接受一個 AbstractControl
類型的參數(其實也就是咱們的 FormControl,而 AbstractControl 爲其父類),而這個方法還要返回 ValidationErrors
,這個 ValidationErrors
的定義以下:
export declare type ValidationErrors = {
[key: string]: any;
};複製代碼
回過頭來再看咱們的這句 (group: FormGroup): {[key: string]: any} => {...}
,你們就應該明白爲何這麼寫了,咱們其實就是在返回一個 ValidatorFn
類型的對象。只不過咱們利用 javascript/typescript
對象展開的特性把 ValidationErrors
寫成了 {[key: string]: any}
。
弄清楚這個函數的邏輯後,咱們怎麼使用呢?很是簡單,先看代碼:
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')});複製代碼
和最初的代碼相比,多了一個參數,那就是 {validator: this.validateEqual('password', 'repeat')}
。FormBuilder 的 group
函數接受兩個參數,第一個就是那串長長的,咱們叫它 controlsConfig
,用於表單控件的構造,以及每一個表單控件的驗證器。可是若是一個驗證器是要計算多個 field
的話,咱們能夠把它做爲整個 group
的驗證器。因此 FormBuilder 的 group
函數還接收第二個參數,這個參數中能夠提供同步驗證器或異步驗證器。一樣仍是一個字典對象,是同步驗證器的話,key
寫成 validator,異步的話寫成 asyncValidator
。
如今咱們能夠保存代碼,啓動 ng serve
到瀏覽器中看一下結果了:
咱們在購物網站常常遇到須要維護多個地址,由於咱們有些商品但願送到公司,有些須要送到家裏,還有些給父母採購的須要送到父母那裏。這就是一個典型的 FormArray 能夠派上用場的場景。全部的這些地址的結構都是同樣的,有省、市、區縣和街道地址,那麼對於處理這樣的場景,咱們來看看在響應式表單中怎麼作。
首先,咱們須要把 HTML 模板改造一下,如今的地址是多項了,因此咱們須要在原來的地址部分外面再套一層,而且聲明成 formArrayName="addrs"
。 FormArray 顧名思義是一個數組,因此咱們要對這個控件數組作一個循環,而後讓每一個數組元素是 FormGroup,只不過此次咱們的 [formGroupName]="i"
是讓 formGroupName
等於該數組元素的索引。
<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>區縣</span>
<select formControlName="area">
<option value="">請選擇區縣</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>複製代碼
改造好模板後,咱們須要在類文件中也作對應處理,去掉原來的 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 數組中。
addAddr(): void {
(<FormArray>this.user.controls['addrs']).push(this.createAddrItem()); } private createAddrItem(): FormGroup { return this.fb.group({ province: [], city: [], area: [], street: [] }) }複製代碼
到這裏咱們的結構就建好了,保存後,到瀏覽器中去試試添加多個地址吧!
首先是可測試能力。模板驅動型表單進行單元測試是比較困難的,由於驗證邏輯是寫在模板中的。但驗證器的邏輯單元測試對於響應式表單來講就很是簡單了,由於你的驗證器無非就是一個函數而已。
固然除了這個優勢,咱們對錶單能夠有徹底的掌控:從初始化表單控件的值、更新和獲取表單值的變化到表單的驗證和提交,這一系列的流程都在程序邏輯控制之下。
並且更重要的是,咱們可使用函數響應式編程的風格來處理各類表單操做,由於響應式表單提供了一系列支持 Observable
的接口 API 。那麼這又能說明什麼呢?有什麼用呢?
首先是不管表單自己仍是控件均可以當作是一系列的基於時間維度的數據流了,這個數據流能夠被多個觀察者訂閱和處理,因爲 valueChanges
自己是個 Observable
,因此咱們就能夠利用 RxJS 提供的豐富的操做符,將一個對數據驗證、處理等的完整邏輯清晰的表達出來。固然如今咱們不會對 RxJS 作深刻的討論,後面有專門針對 RxJS 進行講解的章節。
this.form.valueChanges
.filter((value) => this.user.valid)
.subscribe((value) => {
console.log("如今時刻表單的值爲 ",JSON.stringify(value));
});複製代碼
上面的例子中,咱們取得表單值的變化,而後過濾掉表單存在非法值的狀況,而後輸出表單的值。這只是很是簡單的一個 Rx 應用,隨着邏輯複雜度的增長,咱們後面會見證 Rx 卓越的處理能力。