Angular 4.x Reactive Forms

Angular 4.x 中有兩種表單:html

  • Template-Driven Forms - 模板驅動式表單 (相似於 AngularJS 1.x 中的表單 )react

  • Reactive Forms (Model-Driven Forms) - 響應式表單typescript

Template-Driven Forms (模板驅動表單) ,咱們以前的文章已經介紹過了,瞭解詳細信息,請查看 - Angular 4.x Template-Driven Formsjson

Contents

  • ngModule and reactive formsbootstrap

  • FormControl and FormGroupsegmentfault

  • Implementing our FormGroup model數組

  • Binding our FormGroup model安全

  • Reactive submitapp

  • Reactive error validation單元測試

  • Simplifying with FormBuilder

Form base and interface

Form base

<form novalidate>
  <label>
    <span>Full name</span>
    <input
      type="text"
      name="name"
      placeholder="Your full name">
  </label>
  <div>
    <label>
      <span>Email address</span>
      <input
        type="email"
        name="email"
        placeholder="Your email address">
    </label>
    <label>
      <span>Confirm address</span>
      <input
        type="email"
        name="confirm"
        placeholder="Confirm your email address">
    </label>
  </div>
  <button type="submit">Sign up</button>
</form>

接下來咱們要實現的功能以下:

  • 綁定 name、email、confirm 輸入框的值

  • 爲全部輸入框添加表單驗證功能

  • 顯示驗證異常信息

  • 表單驗證失敗時,不容許進行表單提交

  • 表單提交功能

User interface

// signup.interface.ts
export interface User {
  name: string;
  account: {
    email: string;
    confirm: string;
  }
}

ngModule and reactive forms

在咱們繼續深刻介紹 reactive forms 表單前,咱們必須在 @NgModule 中導入 @angular/forms 庫中的 ReactiveFormsModule

import { ReactiveFormsModule } from '@angular/forms';

@NgModule({
  imports: [
    ...,
    ReactiveFormsModule
  ],
  declarations: [...],
  bootstrap: [...]
})
export class AppModule {}

友情提示:若使用 reactive forms,則導入 ReactiveFormsModule;若使用 template-driven 表單,則導入 FormsModule。

Reactive approach

咱們將基於上面的定義的基礎表單,建立 SignupFormComponent

signup-form.component.ts

import { Component } from '@angular/core';

@Component({
  selector: 'signup-form',
  template: `
    <form novalidate>...</form>
  `
})
export class SignupFormComponent {
  constructor() {}
}

這是一個基礎的組件,在咱們實現上述功能前,咱們須要先介紹 FormControlFormGroupFormBuilder 的概念和使用。

FormControl and FormGroup

咱們先來介紹一下 FormControl 和 FormGroup 的概念:

  • FormControl - 它是一個爲單個表單控件提供支持的類,可用於跟蹤控件的值和驗證狀態,此外還提供了一系列公共API。

使用示例:

ngOnInit() {
  this.myControl = new FormControl('Semlinker');
}
  • FormGroup - 包含是一組 FormControl 實例,可用於跟蹤 FormControl 組的值和驗證狀態,此外也提供了一系列公共API。

使用示例:

ngOnInit() {
  this.myGroup = new FormGroup({
    name: new FormControl('Semlinker'),
    location: new FormControl('China, CN')
  });
}

如今咱們已經建立了 FormControlFormGroup 實例,接下來咱們來看一下如何使用:

<form novalidate [formGroup]="myGroup">
  Name: <input type="text" formControlName="name">
  Location: <input type="text" formControlName="location">
</form>

注意事項:Template-Driven Forms 中介紹的 ngModelname="" 屬性,已經被移除了。這是一件好事,讓咱們的模板更簡潔。

上面示例中,咱們必須使用 [formGroup] 綁定咱們建立的 myGroup 對象,除此以外還要使用 formControlName 指令,綁定咱們建立的 FormControl 控件。此時的表單結構以下:

FormGroup -> 'myGroup'
    FormControl -> 'name'
    FormControl -> 'location'

Implementing our FormGroup model

signup.interface.ts

export interface User {
  name: string;
  account: {
    email: string;
    confirm: string;
  }
}

與之對應的表單結構以下:

FormGroup -> 'user'
    FormControl -> 'name'
    FormGroup -> 'account'
        FormControl -> 'email'
        FormControl -> 'confirm'

是的,咱們能夠建立嵌套的 FormGroup 集合!讓咱們更新一下組件 (不包含初始數據):

import { Component, OnInit } from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms';

@Component({...})
export class SignupFormComponent implements OnInit {
  user: FormGroup;
  ngOnInit() {
    this.user = new FormGroup({
      name: new FormControl(''),
      account: new FormGroup({
        email: new FormControl(''),
        confirm: new FormControl('')
      })
    });
  }
}

若是咱們想要設置初始數據,咱們能夠按照上述示例進行設置。一般狀況下,咱們經過服務端提供的 API 接口來獲取表單的初始信息。

Binding our FormGroup model

如今咱們已經實例化了 FormGroup 模型,是時候綁定到對應的 DOM 元素上了。具體示例以下:

<form novalidate [formGroup]="user">
  <label>
    <span>Full name</span>
    <input
      type="text"
      placeholder="Your full name"
      formControlName="name">
  </label>
  <div formGroupName="account">
    <label>
      <span>Email address</span>
      <input
        type="email"
        placeholder="Your email address"
        formControlName="email">
    </label>
    <label>
      <span>Confirm address</span>
      <input
        type="email"
        placeholder="Confirm your email address"
        formControlName="confirm">
    </label>
  </div>
  <button type="submit">Sign up</button>
</form>

如今 FormGroupFormControl 對象與 DOM 結構的關聯信息以下:

// JavaScript APIs
FormGroup -> 'user'
    FormControl -> 'name'
    FormGroup -> 'account'
        FormControl -> 'email'
        FormControl -> 'confirm'

// DOM bindings
formGroup -> 'user'
    formControlName -> 'name'
    formGroupName -> 'account'
        formControlName -> 'email'
        formControlName -> 'confirm'

當使用模板驅動的表單時,爲了獲取 f.value 表單的值,咱們須要先執行 #f="ngForm" 的操做。而對於使用響應式的表單,咱們能夠經過如下方式,方便的獲取表單的值:

{{ user.value | json }} // { name: '', account: { email: '', confirm: '' }}

Reactive submit

跟模板驅動的表單同樣,咱們能夠經過 ngSubmit 輸出屬性,處理表單的提交邏輯:

<form novalidate (ngSubmit)="onSubmit(user)" [formGroup]="user">
  ...
</form>

須要注意的是,咱們使用 user 對象做爲 onSubmit() 方法的參數,這使得咱們能夠獲取表單對象的相關信息,具體處理邏輯以下:

export class SignupFormComponent {
  user: FormGroup;
  onSubmit({ value, valid }: { value: User, valid: boolean }) {
    console.log(value, valid);
  }
}

上面代碼中,咱們使用 Object destructuring (對象解構) 的方式,從user 對象中獲取 valuevalid 屬性的值。其中 value 的值,就是 user.value 的值。在實際應用中,咱們是不須要傳遞 user 參數的:

export class SignupFormComponent {
  user: FormGroup;
  onSubmit() {
    console.log(this.user.value, this.user.valid);
  }
}

表單的數據綁定方式和提交邏輯已經介紹完了,是該介紹表單實際應用中,一個重要的環節 — 表單驗證。

Reactive error validation

接下來咱們來爲表單添加驗證規則,首先咱們須要從 @angular/forms 中導入 Validators。具體使用示例以下:

ngOnInit() {
  this.user = new FormGroup({
    name: new FormControl('', [Validators.required, Validators.minLength(2)]),
    account: new FormGroup({
      email: new FormControl('', Validators.required),
      confirm: new FormControl('', Validators.required)
    })
  });
}

經過以上示例,咱們能夠看出,若是表單控制包含多種驗證規則,可使用數組聲明多種驗證規則。若只包含一種驗證規則,直接聲明就好。經過這種方式,咱們就不須要在模板的輸入控件中添加 required 屬性。接下來咱們來添加表單驗證失敗時,不容許進行表單提交功能:

<form novalidate (ngSubmit)="onSubmit(user)" [formGroup]="user">
  ...
  <button type="submit" [disabled]="user.invalid">Sign up</button>
</form>

那麼問題來了,咱們要如何獲取表單控件的驗證信息?咱們可使用模板驅動表單中介紹的方式,具體以下:

<form novalidate [formGroup]="user">
  {{ user.controls.name?.errors | json }}
</form>

友情提示:?.prop 稱爲安全導航操做符,用於告訴 Angular prop 的值可能不存在。

此外咱們也可使用 FormGroup 對象提供的 API,來獲取表單控件驗證的錯誤信息:

<form novalidate [formGroup]="user">
  {{ user.get('name').errors | json }}
</form>

如今咱們來看一下完整的代碼:

import { Component, OnInit } from '@angular/core';
import { FormControl, FormGroup, Validators } from '@angular/forms';
import { User } from './signup.interface';

@Component({
  selector: 'signup-form',
  template: `
    <form novalidate (ngSubmit)="onSubmit(user)" [formGroup]="user">
      <label>
        <span>Full name</span>
        <input type="text" placeholder="Your full name" formControlName="name">
      </label>
      <div class="error" *ngIf="user.get('name').hasError('required') && 
            user.get('name').touched">
        Name is required
      </div>
      <div class="error" *ngIf="user.get('name').hasError('minlength') && 
            user.get('name').touched">
        Minimum of 2 characters
      </div>
      <div formGroupName="account">
        <label>
          <span>Email address</span>
          <input type="email" placeholder="Your email address" formControlName="email">
        </label>
        <div
          class="error"
          *ngIf="user.get('account').get('email').hasError('required') && 
             user.get('account').get('email').touched">
          Email is required
        </div>
        <label>
          <span>Confirm address</span>
          <input type="email" placeholder="Confirm your email address" 
             formControlName="confirm">
        </label>
        <div
          class="error"
          *ngIf="user.get('account').get('confirm').hasError('required') && 
             user.get('account').get('confirm').touched">
          Confirming email is required
        </div>
      </div>
      <button type="submit" [disabled]="user.invalid">Sign up</button>
    </form>
  `
})
export class SignupFormComponent implements OnInit {
  user: FormGroup;
  constructor() {}
  ngOnInit() {
    this.user = new FormGroup({
      name: new FormControl('', [Validators.required, Validators.minLength(2)]),
      account: new FormGroup({
        email: new FormControl('', Validators.required),
        confirm: new FormControl('', Validators.required)
      })
    });
  }
  onSubmit({ value, valid }: { value: User, valid: boolean }) {
    console.log(value, valid);
  }
}

功能是實現了,但建立 FormGroup 對象的方式有點繁瑣,Angular 團隊也意識到這點,所以爲咱們提供 FormBuilder ,來簡化上面的操做。

Simplifying with FormBuilder

首先咱們須要從 @angular/forms 中導入 FormBuilder

import { FormBuilder, FormGroup, Validators } from '@angular/forms';

export class SignupFormComponent implements OnInit {
  user: FormGroup;
  constructor(private fb: FormBuilder) {}
  ...
}

而後咱們使用 FormBuilder 對象提供的 group() 方法,來建立 FormGroupFormControl 對象:

調整前的代碼 (未使用FormBuilder):

ngOnInit() {
  this.user = new FormGroup({
    name: new FormControl('', [Validators.required, Validators.minLength(2)]),
    account: new FormGroup({
      email: new FormControl('', Validators.required),
      confirm: new FormControl('', Validators.required)
    })
  });
}

調整後的代碼 (使用FormBuilder):

ngOnInit() {
  this.user = this.fb.group({
    name: ['', [Validators.required, Validators.minLength(2)]],
    account: this.fb.group({
      email: ['', Validators.required],
      confirm: ['', Validators.required]
    })
  });
}

對比一下調整前和調整後的代碼,是否是感受一會兒方便了許多。此時更新完後完整的代碼以下:

@Component({...})
export class SignupFormComponent implements OnInit {
  user: FormGroup;
  constructor(private fb: FormBuilder) {}
  ngOnInit() {
    this.user = this.fb.group({
      name: ['', [Validators.required, Validators.minLength(2)]],
      account: this.fb.group({
        email: ['', Validators.required],
        confirm: ['', Validators.required]
      })
    });
  }
  onSubmit({ value, valid }: { value: User, valid: boolean }) {
    console.log(value, valid);
  }
}

我有話說

Template-Driven Forms vs Reactive Forms

Template-Driven Forms (模板驅動表單) 的特色

  • 使用方便

  • 適用於簡單的場景

  • 經過 [(ngModel)] 實現數據雙向綁定

  • 最小化組件類的代碼

  • 不易於單元測試

Reactive Forms (響應式表單) 的特色

  • 比較靈活

  • 適用於複雜的場景

  • 簡化了HTML模板的代碼,把驗證邏輯抽離到組件類中

  • 方便的跟蹤表單控件值的變化

  • 易於單元測試

參考資源

相關文章
相關標籤/搜索