細(xì)說 Angular 2+ 的表單(二):響應(yīng)式表單

細(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
  • 但同時每個控件也去掉了驗證條件,比如 requiredminlength
  • 在地址分組中用 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 。比如下面的 keypassword,對應(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 , grouparray ,分別對應(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é)果了:

響應(yīng)式表單對于多值驗證的處理
響應(yīng)式表單對于多值驗證的處理

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)就建好了,保存后,到瀏覽器中去試試添加多個地址吧!

FormArray 處理結(jié)構(gòu)相同的多組表單項
FormArray 處理結(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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,333評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,491評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 176,263評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,946評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 71,708評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,186評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,255評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,409評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,939評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,774評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,976評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,518評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,209評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,641評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,872評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,650評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 47,958評論 2 373

推薦閱讀更多精彩內(nèi)容