Angular4 響應式表單應用以及驗證

基礎的表單類

  • AbstractControl是三個具體表單類的抽象基類。 併爲它們提供了一些共同的行爲和屬性,其中有些是可觀察對象(Observable)。
  • FormControl 用於跟蹤一個單獨的表單控件的值和有效性狀態。它對應於一個HTML表單控件,好比輸入框和下拉框。
  • FormGroup用於 跟蹤一組AbstractControl的實例的值和有效性狀態。 該組的屬性中包含了它的子控件。 組件中的頂級表單就是一個FormGroup。
  • FormArray用於跟蹤AbstractControl實例組成的有序數組的值和有效性狀態。

添加FormGroup

一般,若是有多個FormControl,咱們會但願把它們註冊進一個父FormGroup中:html

src/app/hero-detail.component.ts

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

export class HeroDetailComponent2 {
  heroForm = new FormGroup ({
    name: new FormControl()
  });
}

如今咱們改完了這個類,該把它映射到模板中了,把hero-detail.component.html改爲這樣:react

src/app/hero-detail.component.html

<h2>Hero Detail</h2>
<h3><i>FormControl in a FormGroup</i></h3>
<form [formGroup]="heroForm" novalidate>
  <div class="form-group">
    <label class="center-block">Name:
      <input class="form-control" formControlName="name">
    </label>
  </div>
</form>
  • 注意,如今單行輸入框位於一個form元素中。<form>元素上的novalidate屬性會阻止瀏覽器使用原生HTML中的表單驗證器。
  • formGroup是一個響應式表單的指令,它拿到一個現有FormGroup實例,並把它關聯到一個HTML元素上。 這種狀況下,它關聯到的是form元素上的FormGroup實例heroForm。
  • 沒有父FormGroup的時候,[formControl]="name"也能正常工做,由於該指令能夠獨立工做,也就是說,不在FormGroup中時它也能用。有了FormGroup,name輸入框就須要再添加一個語法formControlName=name,以便讓它關聯到類中正確的FormControl上。這個語法告訴Angular,查閱父FormGroup(這裏是heroForm),而後在這個FormGroup中查閱一個名叫name的FormControl。

表單模型概覽

要想知道表單模型是什麼樣的,請在hero-detail.component.html的form標籤緊後面添加以下代碼:正則表達式

src/app/hero-detail.component.html
 content_copy
<p>Form value: {{ heroForm.value | json }}</p>
<p>Form status: {{ heroForm.status | json }}</p>

FormBuilder簡介

如今,咱們遵循下列步驟用FormBuilder來把HeroDetailComponent重構得更加容易讀寫:express

  • 明確把heroForm屬性的類型聲明爲FormGroup,稍後咱們會初始化它。
  • 把FormBuilder注入到構造函數中。
  • 添加一個名叫createForm的新方法,它會用FormBuilder來定義heroForm。
  • 在構造函數中調用createForm。
src/app/hero-detail.component.ts

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

export class HeroDetailComponent3 {
  heroForm: FormGroup; // <--- heroForm is of type FormGroup

  constructor(private fb: FormBuilder) { // <--- inject FormBuilder
    this.createForm();
  }

  createForm() {
    this.heroForm = this.fb.group({
      name: '', // <--- the FormControl called "name"
    });
  }
}

FormBuilder.group是一個用來建立FormGroup的工廠方法,它接受一個對象,對象的鍵和值分別是FormControl的名字和它的定義。 在這個例子中,name控件的初始值是空字符串。json

Validators.required 驗證器

要想讓name這個FormControl是必須的,請把FormGroup中的name屬性改成一個數組。第一個條目是name的初始值,第二個是required驗證器:Validators.required。數組

src/app/hero-detail.component.ts 

this.heroForm = this.fb.group({
  name: ['', Validators.required ],
});

多級FormGroup

用FormBuilder在這個名叫heroForm的組件中建立一個FormGroup,並把它用做父FormGroup。 再次使用FormBuilder建立一個子級FormGroup,其中包括這些住址控件。把結果賦值給父FormGroup中新的address屬性:瀏覽器

src/app/hero-detail.component.ts (excerpt)
 
export class HeroDetailComponent5 {
  heroForm: FormGroup;
  states = states;

  constructor(private fb: FormBuilder) {
    this.createForm();
  }

  createForm() {
    this.heroForm = this.fb.group({ // <-- the parent FormGroup
      name: ['', Validators.required ],
      address: this.fb.group({ // <-- the child FormGroup
        street: '',
        city: '',
        state: '',
        zip: ''
      }),
      power: '',
      sidekick: ''
    });
  }
}

在hero-detail.component.html中,把與住址有關的FormControl包裹進一個div中。 往這個div上添加一個formGroupName指令,而且把它綁定到"address"上。 這個address屬性是一個FormGroup,它的父FormGroup就是heroForm:服務器

src/app/hero-detail.component.html (address)

<div formGroupName="address" class="well well-lg">
  <h4>Secret Lair</h4>
  <div class="form-group">
    <label class="center-block">Street:
      <input class="form-control" formControlName="street">
    </label>
  </div>
  <div class="form-group">
    <label class="center-block">City:
      <input class="form-control" formControlName="city">
    </label>
  </div>
  <div class="form-group">
    <label class="center-block">State:
      <select class="form-control" formControlName="state">
        <option *ngFor="let state of states" [value]="state">{{state}}</option>
      </select>
    </label>
  </div>
  <div class="form-group">
    <label class="center-block">Zip Code:
      <input class="form-control" formControlName="zip">
    </label>
  </div>
</div>

查看FormControl的屬性

可使用.get()方法來提取表單中一個單獨FormControl的狀態。 咱們能夠在組件類中這麼作,或者經過往模板中添加下列代碼來把它顯示在頁面中,就添加在{{form.value | json}}插值表達式的緊後面:app

src/app/hero-detail.component.html

<p>Name value: {{ heroForm.get('name').value }}</p>
<p>Street value: {{ heroForm.get('address.street').value}}</p>

數據模型與表單模型

來自服務器的hero就是數據模型,而FormControl的結構就是表單模型。異步

組件必須把數據模型中的英雄值複製到表單模型中,這裏隱含着兩個很是重要的點:

  • 開發人員必須理解數據模型是如何映射到表單模型中的屬性的
  • 戶修改時的數據流是從DOM元素流向表單模型的,而不是數據模型,表單控件永遠不會修改數據模型

一般只會展示數據模型的一個子集,表單模型的形態越接近數據模型,事情就會越簡單,在HeroDetailComponent中,這兩個模型是很是接近的,data-model.ts中的Hero定義:

export class Hero {
  id = 0;
  name = '';
  addresses: Address[];
}

export class Address {
  street = '';
  city   = '';
  state  = '';
  zip    = '';
}

組件的FormGroup定義:

src/app/hero-detail.component.ts 

this.heroForm = this.fb.group({
  name: ['', Validators.required ],
  address: this.fb.group({
    street: '',
    city: '',
    state: '',
    zip: ''
  }),
  power: '',
  sidekick: ''
});

在這些模型中有兩點顯著的差別:

  • Hero有一個id。表單模型中則沒有,由於咱們一般不會把主鍵展現給用戶
  • Hero有一個住址數組。這個表單模型只表示了一個住址
    重構一下address這個FormGroup定義,來讓它更簡潔清晰,代碼以下:
src/app/hero-detail.component.ts 

this.heroForm = this.fb.group({
  name: ['', Validators.required ],
  address: this.fb.group(new Address()), // <-- a FormGroup with a new address
  power: '',
  sidekick: ''
});

使用setValue和patchValue來操縱表單模型

setValue 方法

藉助setValue,咱們能夠當即設置每一個表單控件的值,只要把與表單模型的屬性精確匹配的數據模型傳進去就能夠了

src/app/hero-detail.component.ts 

this.heroForm.setValue({
  name:    this.hero.name,
  address: this.hero.addresses[0] || new Address()
});

setValue方法會在賦值給任何表單控件以前先檢查數據對象的值。

它不會接受一個與FormGroup結構不一樣或缺乏表單組中任何一個控件的數據對象。 這種方式下,若是咱們有什麼拼寫錯誤或控件嵌套的不正確,它就能返回一些有用的錯誤信息。 patchValue會默默地失敗。

而setValue會捕獲錯誤,並清晰的報告它。

注意,你幾乎能夠把這個hero用做setValue的參數,由於它的形態與組件的FormGroup結構是很是像的。

咱們如今只能顯示英雄的第一個住址,不過咱們還必須考慮hero徹底沒有住址的可能性。 下面的例子解釋瞭如何在數據對象參數中對address屬性進行有條件的設置:

address: this.hero.addresses[0] || new Address()

patchValue 方法

藉助patchValue,咱們能夠經過提供一個只包含要更新的控件的鍵值對象來把值賦給FormGroup中的指定控件,這個例子只會設置表單的name控件:

this.heroForm.patchValue({
  name: this.hero.name
});

藉助patchValue,咱們能夠更靈活地解決數據模型和表單模型之間的差別。 可是和setValue不一樣,patchValue不會檢查缺失的控件值,而且不會拋出有用的錯誤信息。

何時設置表單的模型值(ngOnChanges)

何時設置表單的模型值取決於組件什麼時候獲得數據模型的值

HeroListComponent組件把英雄的名字顯示給用戶,當用戶點擊一個英雄時,列表組件把所選的英雄經過輸入屬性hero傳給HeroDetailComponent:

hero-list.component.html (simplified)

<nav>
  <a *ngFor="let hero of heroes | async" (click)="select(hero)">{{hero.name}}</a>
</nav>

<div *ngIf="selectedHero">
  <app-hero-detail [hero]="selectedHero"></app-hero-detail>
</div>

這種方式下,每當用戶選擇一個新英雄時,HeroDetailComponent中的hero值就會發生變化。 咱們能夠在ngOnChanges鉤子中調用setValue,就像例子中所演示的那樣, 每當輸入屬性hero發生變化時,Angular就會調用它。

src/app/hero-detail.component.ts (ngOnchanges)

ngOnChanges()
  this.heroForm.setValue({
    name:    this.hero.name,
    address: this.hero.addresses[0] || new Address()
  });
}

重置表單的標識

咱們應該在更換英雄的時候重置表單,以便來自前一個英雄的控件值被清除,而且其狀態被恢復爲pristine(原始)狀態。 咱們能夠在ngOnChanges的頂部調用reset,就像這樣:

src/app/hero-detail-7.component.ts

this.heroForm.reset();

reset方法有一個可選的state值,讓咱們能在重置狀態的同時順便設置控件的值。 在內部實現上,reset會把該參數傳給了setValue。 略微重構以後,ngOnChanges會變成這樣:

src/app/hero-detail.component.ts (ngOnchanges - revised)

ngOnChanges() {
  this.heroForm.reset({
    name: this.hero.name,
    address: this.hero.addresses[0] || new Address()
  });
}

使用FormArray來表示FormGroup數組

src/app/hero-detail.component.ts
 
this.heroForm = this.fb.group({
  name: ['', Validators.required ],
  secretLairs: this.fb.array([]), // <-- secretLairs as an empty FormArray
  power: '',
  sidekick: ''
});

把表單的控件名從address改成secretLairs讓咱們遇到了一個重要問題:表單模型與數據模型再也不匹配了。
### 初始化FormArray型的secretLairs
下面的setAddresses方法把secretLairs數組替換爲一個新的FormArray,使用一組表示英雄地址的FormGroup來進行初始化:

src/app/hero-detail.component.ts

setAddresses(addresses: Address[]) {
  const addressFGs = addresses.map(address => this.fb.group(address));
  const addressFormArray = this.fb.array(addressFGs);
  this.heroForm.setControl('secretLairs', addressFormArray);
}

注意,咱們使用FormGroup.setControl方法,而不是setValue方法來設置前一個FormArray。 咱們所要替換的是控件,而不是控件的值。

### 獲取FormArray
使用FormGroup.get方法來獲取到FormArray的引用:

get secretLairs(): FormArray {
  return this.heroForm.get('secretLairs') as FormArray;
};

### 顯示FormArray
訣竅在於要知道如何編寫*ngFor。主要有三點:

  • 在*ngFor的<div>以外套上另外一個包裝<div>,而且把它的formArrayName指令設爲"secretLairs"。 這一步爲內部的表單控件創建了一個FormArray型的secretLairs做爲上下文,以便重複渲染HTML模板。
  • 這些重複條目的數據源是FormArray.controls而不是FormArray自己。 每一個控件都是一個FormGroup型的地址對象,與之前的模板HTML所指望的格式徹底同樣。
  • 每一個被重複渲染的FormGroup都須要一個獨一無二的formGroupName,它必須是FormGroup在這個FormArray中的索引。 咱們將複用這個索引,以便爲每一個地址組合出一個獨一無二的標籤。
src/app/hero-detail.component.html (excerpt)
 
<div formArrayName="secretLairs" class="well well-lg">
  <div *ngFor="let address of secretLairs.controls; let i=index" [formGroupName]="i" >
    <!-- The repeated address template -->
    <h4>Address #{{i + 1}}</h4>
    <div style="margin-left: 1em;">
      <div class="form-group">
        <label class="center-block">Street:
          <input class="form-control" formControlName="street">
        </label>
      </div>
      <div class="form-group">
        <label class="center-block">City:
          <input class="form-control" formControlName="city">
        </label>
      </div>
      <div class="form-group">
        <label class="center-block">State:
          <select class="form-control" formControlName="state">
            <option *ngFor="let state of states" [value]="state">{{state}}</option>
          </select>
        </label>
      </div>
      <div class="form-group">
        <label class="center-block">Zip Code:
          <input class="form-control" formControlName="zip">
        </label>
      </div>
    </div>
    <br>
    <!-- End of the repeated address template -->
  </div>
</div>

### 把新的address添加到FormArray中

addLair() {
  this.heroForm.get('secretLairs').push(this.fb.group(new Address()));
}

監視控件的變化

每當用戶在父組件HeroListComponent中選取了一個英雄,Angular就會調用一次ngOnChanges。 選取英雄會修改輸入屬性HeroDetailComponent.hero。

當用戶修改英雄的名字或祕密小屋時,Angular並不會調用ngOnChanges。 幸運的是,咱們能夠經過訂閱表單控件的屬性之一來了解這些變化,此屬性會發出變動通知。

有一些屬性,好比valueChanges,能夠返回一個RxJS的Observable對象。 要監聽控件值的變化,咱們並不須要對RxJS的Observable瞭解更多。

添加下列方法,以監聽姓名這個FormControl中值的變化:

src/app/hero-detail.component.ts (logNameChange)

nameChangeLog: string[] = [];
logNameChange() {
  const nameControl = this.heroForm.get('name');
  nameControl.valueChanges.forEach(
    (value: string) => this.nameChangeLog.push(value)
  );
}

在構造函數中調用它,就在建立表單的代碼以後:

src/app/hero-detail-8.component.ts
 
constructor(private fb: FormBuilder) {
  this.createForm();
  this.logNameChange();
}

保存表單數據

### 保存
當用戶提交表單時,HeroDetailComponent會把英雄實例的數據模型傳給所注入進來的HeroService的一個方法來進行保存:

src/app/hero-detail.component.ts (onSubmit)
 
onSubmit() {
  this.hero = this.prepareSaveHero();
  this.heroService.updateHero(this.hero).subscribe(/* error handling */);
  this.ngOnChanges();
}

原始的hero中有一些保存以前的值,用戶的修改仍然是在表單模型中。 因此咱們要根據原始英雄(根據hero.id找到它)的值組合出一個新的hero對象,並用prepareSaveHero助手來深層複製變化後的模型值。

src/app/hero-detail.component.ts (prepareSaveHero)

prepareSaveHero(): Hero {
  const formModel = this.heroForm.value;

  // deep copy of form model lairs
  const secretLairsDeepCopy: Address[] = formModel.secretLairs.map(
    (address: Address) => Object.assign({}, address)
  );

  // return new `Hero` object containing a combination of original hero value(s)
  // and deep copies of changed form model values
  const saveHero: Hero = {
    id: this.hero.id,
    name: formModel.name as string,
    // addresses: formModel.secretLairs // <-- bad!
    addresses: secretLairsDeepCopy
  };
  return saveHero;
}

地址的深層複製

咱們已經把formModel.secretLairs賦值給了saveHero.addresses(參見注釋掉的部分), saveHero.addresses數組中的地址和formModel.secretLairs中的會是同一個對象。 用戶隨後對小屋所在街道的修改將會改變saveHero中的街道地址。

但prepareSaveHero方法會製做表單模型中的secretLairs對象的複本,所以實際上並無修改原有對象。

### 丟棄(撤銷修改)
丟棄很容易。只要從新執行ngOnChanges方法就能夠拆而,它會從新從原始的、未修改過的hero數據模型來構建出表單模型:

src/app/hero-detail.component.ts (revert)

revert() { this.ngOnChanges(); }

響應式表單的驗證

在響應式表單中,真正的源碼都在組件類中。咱們不該該經過模板上的屬性來添加驗證器,而應該在組件類中直接把驗證器函數添加到表單控件模型上(FormControl)。而後,一旦控件發生了變化,Angular 就會調用這些函數。

驗證器函數

有兩種驗證器函數:同步驗證器和異步驗證器。

  • 同步驗證器函數接受一個控件實例,而後返回一組驗證錯誤或null。咱們能夠在實例化一個FormControl時把它做爲構造函數的第二個參數傳進去。
  • 異步驗證器函數接受一個控件實例,並返回一個承諾(Promise)或可觀察對象(Observable),它們稍後會發出一組驗證錯誤或者null。咱們能夠在實例化一個FormControl時把它做爲構造函數的第三個參數傳進去。

注意:出於性能方面的考慮,只有在全部同步驗證器都經過以後,Angular 纔會運行異步驗證器。當每個異步驗證器都執行完以後,纔會設置這些驗證錯誤。

內置驗證器

模板驅動表單中可用的那些屬性型驗證器(如required、minlength等)對應於Validators類中的同名函數。要想查看內置驗證器的全列表,參見 API 參考手冊中的Validators部分。

reactive/hero-form-reactive.component.ts (validator functions)

ngOnInit(): void {
  this.heroForm = new FormGroup({
    'name': new FormControl(this.hero.name, [
      Validators.required,
      Validators.minLength(4),
      forbiddenNameValidator(/bob/i) // <-- Here's how you pass in the custom validator.
    ]),
    'alterEgo': new FormControl(this.hero.alterEgo),
    'power': new FormControl(this.hero.power, Validators.required)
  });
}

get name() { return this.heroForm.get('name'); }

get power() { return this.heroForm.get('power'); }

注意

  • name控件設置了兩個內置驗證器:Validators.required 和 Validators.minLength(4)。
  • 因爲這些驗證器都是同步驗證器,所以咱們要把它們做爲第二個參數傳進去。
  • 能夠經過把這些函數放進一個數組後傳進去,能夠支持多重驗證器。
  • 這個例子添加了一些getter方法。在響應式表單中,咱們一般會經過它所屬的控件組(FormGroup)的get方法來訪問表單控件,但有時候爲模板定義一些getter做爲簡短形式。
reactive/hero-form-reactive.component.html (name with error msg)

<input id="name" class="form-control"
       formControlName="name" required >

<div *ngIf="name.invalid && (name.dirty || name.touched)"
     class="alert alert-danger">

  <div *ngIf="name.errors.required">
    Name is required.
  </div>
  <div *ngIf="name.errors.minlength">
    Name must be at least 4 characters long.
  </div>
  <div *ngIf="name.errors.forbiddenName">
    Name cannot be Bob.
  </div>
</div>

自定義驗證器

shared/forbidden-name.directive.ts (forbiddenNameValidator)

/** A hero's name can't match the given regular expression */
export function forbiddenNameValidator(nameRe: RegExp): ValidatorFn {
  return (control: AbstractControl): {[key: string]: any} => {
    const forbidden = nameRe.test(control.value);
    return forbidden ? {'forbiddenName': {value: control.value}} : null;
  };
}

這個函數其實是一個工廠,它接受一個用來檢測指定名字是否已被禁用的正則表達式,並返回一個驗證器函數。

在本例中,禁止的名字是「bob」; 驗證器會拒絕任何帶有「bob」的英雄名字。 在其餘地方,只要配置的正則表達式能夠匹配上,它可能拒絕「alice」或者任何其餘名字。

forbiddenNameValidator工廠函數返回配置好的驗證器函數。 該函數接受一個Angular控制器對象,並在控制器值有效時返回null,或無效時返回驗證錯誤對象。 驗證錯誤對象一般有一個名爲驗證祕鑰(forbiddenName)的屬性。其值爲一個任意詞典,咱們能夠用來插入錯誤信息

添加響應式表單

在響應式表單組件中,添加自定義驗證器至關簡單。你所要作的一切就是直接把這個函數傳給 FormControl 。

reactive/hero-form-reactive.component.ts (validator functions)

this.heroForm = new FormGroup({
  'name': new FormControl(this.hero.name, [
    Validators.required,
    Validators.minLength(4),
    forbiddenNameValidator(/bob/i) // <-- Here's how you pass in the custom validator.
  ]),
  'alterEgo': new FormControl(this.hero.alterEgo),
  'power': new FormControl(this.hero.power, Validators.required)
});
相關文章
相關標籤/搜索