Angular 4.x Forms patchValue and setValue

在 Angular 4.x 中有多種方式能夠更新表單的值,對於使用響應式表單的場景,咱們能夠經過框架內部提供的 API ,(如 patchValue 和 setValue )方便地更新表單的值。這篇文章咱們將介紹如何使用 patchValue 和 setValue 方法更新表單的值,此外還會進一步介紹它們之間的差別。javascript

Reactive Form Setup

app.module.tsjava

import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { ReactiveFormsModule } from '@angular/forms';

import { AppComponent } from './app.component';
import { EventFormComponent } from './event-form.component';

@NgModule({
  imports: [BrowserModule, ReactiveFormsModule],
  declarations: [AppComponent, EventFormComponent],
  bootstrap: [AppComponent],
  schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class AppModule { }

app.component.tstypescript

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

@Component({
  selector: 'exe-app',
  template: `
   <event-form></event-form>
  `,
})
export class AppComponent {}

event-form.component.tsbootstrap

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

@Component({
    selector: 'event-form',
    template: `
    <form novalidate (ngSubmit)="onSubmit(form)" [formGroup]="form">
      <div>
        <label>
          <span>Full name</span>
          <input type="text" class="input" formControlName="name">
        </label>
        <div formGroupName="event">
          <label>
            <span>Event title</span>
            <input type="text" class="input" formControlName="title">
          </label>
          <label>
            <span>Event location</span>
            <input type="text" class="input" formControlName="location">
          </label>
        </div>
      </div>
      <div>
        <button type="submit" [disabled]="form.invalid">
          Submit
        </button>
      </div>
    </form>
  `,
})
export class EventFormComponent implements OnInit {
    form: FormGroup;

    constructor(public fb: FormBuilder) { }

    ngOnInit() {
        this.form = this.fb.group({
            name: ['', Validators.required],
            event: this.fb.group({
                title: ['', Validators.required],
                location: ['', Validators.required]
            })
        });
    }

    onSubmit({ value, valid }: { value: any, valid: boolean }) { }
}

patchValue

咱們先來介紹 patchValue() 方法,而後在介紹 setValue() 方法。使用 patchValue() 方法會比使用 setValue() 方法更好,爲何這麼說呢?咱們來看一下源碼就知道答案了。數組

// angular2/packages/forms/src/model.ts
export class FormGroup extends AbstractControl {
   ...
   patchValue(
     value: {[key: string]: any},{onlySelf, emitEvent}: 
                 {onlySelf?: boolean, emitEvent?: boolean} = {}): void {
      Object.keys(value).forEach(name => {
        if (this.controls[name]) {
          this.controls[name].patchValue(value[name], {onlySelf: true, emitEvent});
        }
      });
      this.updateValueAndValidity({onlySelf, emitEvent});
   }
}

// 使用示例
const form = new FormGroup({
   first: new FormControl(),
   last: new FormControl()
});

console.log(form.value);   // {first: null, last: null}

form.patchValue({first: 'Nancy'});
console.log(form.value);   // {first: 'Nancy', last: null}

從源碼中咱們能夠看出,patchValue() 方法會獲取輸入參數對象的全部 key 值,而後循環調用內部控件的 patchValue() 方法,具體代碼以下:angular2

Object.keys(value).forEach(name => {
  if (this.controls[name]) {
     this.controls[name].patchValue(value[name], {onlySelf: true, emitEvent});
  }
});

首先,Object.keys() 會返回對象 key 值的數組,例如:app

const man = {name : 'Semlinker', age: 30};
Object.keys(man); // ['name', 'age']

此外 this.controls 包含了 FormGroup 對象中的全部 FormControl 控件,咱們能夠經過 this.controls[name] 方式,訪問到 name 對應的控件對象。框架

如今讓咱們來回顧一下上面的示例中建立 FormGroup 對象的相關代碼:async

this.form = this.fb.group({
  name: ['', Validators.required],
  event: this.fb.group({
    title: ['', Validators.required],
    location: ['', Validators.required]
  })
});

與之相對應的對象模型以下:函數

{
  name: '',
  event: {
    title: '',
    location: ''
  }
}

所以要更新該模型的值,咱們能夠利用 FormGroup 對象的 patchValue() 方法:

this.form.patchValue({
  name: 'Semlinker',
  event: {
    title: 'Angular 4.x\'s Road',
    location: 'Xiamen'
  }
});

以上代碼將會經過循環的方式,更新每一個 FormControl 控件。接下來咱們看一下 FormControl 中 patchValue() 方法的具體實現:

patchValue(value: any, options: {
    onlySelf?: boolean,
    emitEvent?: boolean,
    emitModelToViewChange?: boolean,
    emitViewToModelChange?: boolean
  } = {}): void {
    this.setValue(value, options);
}

忽略全部的函數參數和類型,它所作的就是調用 setValue() 方法,設置控件的值。另外使用 patchValue() 方法有什麼好處呢?假設咱們使用 firebase,那麼當咱們從 API 接口獲取數據對象時,對象內部可能會包含 $exists$key 屬性。而當咱們直接使用返回的數據對象做爲參數,直接調用 patchValue() 方法時,不會拋出任何異常:

this.form.patchValue({
  $exists: function () {},
  $key: '-KWihhw-f1kw-ULPG1ei',
  name: 'Semlinker',
  event: {
    title: 'Angular 4.x\'s Road',
    location: 'Xiamen'
  }
});

其實沒有拋出異常的緣由,是由於在 patchValue() 內部循環時,咱們有使用 if 語句進行條件判斷。那好,如今咱們開始來介紹 setValue() 方法。

setValue

首先,咱們來看一下 FormGroup 類中的 setValue() 方法的具體實現:

setValue(
  value: {[key: string]: any},
  {onlySelf, emitEvent}: {onlySelf?: boolean, emitEvent?: boolean} = {}): void {
      this._checkAllValuesPresent(value);
      Object.keys(value).forEach(name => {
        this._throwIfControlMissing(name);
        this.controls[name].setValue(value[name], {onlySelf: true, emitEvent});
      });
      this.updateValueAndValidity({onlySelf, emitEvent});
}

// 使用示例
const form = new FormGroup({
    first: new FormControl(),
    last: new FormControl()
});
console.log(form.value);   // {first: null, last: null}

form.setValue({first: 'Nancy', last: 'Drew'});
console.log(form.value);   // {first: 'Nancy', last: 'Drew'}

跟 patchValue() 方法同樣,咱們內部也是包含一個 Object.keys() 的循環,但在循環開始以前,咱們會先調用 _checkAllValuesPresent() 方法,對輸入值進行校驗。 另外 _checkAllValuesPresent() 方法的具體實現以下:

_checkAllValuesPresent(value: any): void {
    this._forEachChild((control: AbstractControl, name: string) => {
      if (value[name] === undefined) {
        throw new Error(`Must supply a value for form control with name: '${name}'.`);
      }
    });
}

該方法內部經過 _forEachChild() 遍歷內部的 FormControl 控件,來確保咱們在調用 setValue() 方法時,設置的參數對象中,會包含全部控件的配置信息。若是 name 對應的配置信息不存在,則會拋出異常。

_checkAllValuesPresent() 驗證經過後,Angular 會進入 Object.keys() 循環,此外在調用 setValue() 方法前,還會優先調用 _throwIfControlMissing() 判斷控件是否存在,該方法的實現以下:

_throwIfControlMissing(name: string): void {
    if (!Object.keys(this.controls).length) {
      throw new Error(`
        There are no form controls registered with this group yet.  
        If you're using ngModel,
        you may want to check next tick (e.g. use setTimeout).
      `);
    }
    if (!this.controls[name]) {
      throw new Error(`Cannot find form control with name: ${name}.`);
    }
}

上面代碼首先判斷 this.controls 是否存在,若是存在進一步判斷 name 對應的 FormControl 控件是否存在。當 _throwIfControlMissing() 驗證經過後,纔會最終調用 FormControl 控件的 setValue() 方法:

this.controls[name].setValue(value[name], {onlySelf: true, emitEvent});

咱們來看一下 FormControl 類中,setValue() 方法的具體實現:

setValue(value: any, {onlySelf, emitEvent, emitModelToViewChange,     
    emitViewToModelChange}: {
         onlySelf?: boolean,
        emitEvent?: boolean,
        emitModelToViewChange?: boolean,
        emitViewToModelChange?: boolean
  } = {}): void {
    this._value = value;
    if (this._onChange.length && emitModelToViewChange !== false) {
      this._onChange.forEach((changeFn) => 
            changeFn(this._value, emitViewToModelChange !== false));
    }
    this.updateValueAndValidity({onlySelf, emitEvent});
}

該方法的第一個參數,就是咱們要設置的值,第二個參數是一個對象:

  • onlySelf:若該值爲 true,當控件的值發生變化後,只會影響當前控件的驗證狀態,而不會影響到它的父組件。默認值是 false。

  • emitEvent:若該值爲 true,當控件的值發生變化後,將會觸發 valueChanges 事件。默認值是 true

  • emitModelToViewChange:若該值爲 true,當控件的值發生變化時,將會把新值經過 onChange 事件通知視圖層。若未指定 emitModelToViewChange 的值,這是默認的行爲。

  • emitViewToModelChange:若該值爲 true,ngModelChange 事件將會被觸發,用於更新模型。若未指定 emitViewToModelChange 的值,這是默認的行爲。

其實僅僅經過上面的代碼,咱們仍是沒徹底搞清楚 setValue() 方法內部真正執行流程。如咱們不知道如何註冊 changeFn 函數和 updateValueAndValidity() 方法的內部處理邏輯,接下來咱們先來看一下如何註冊 changeFn 函數:

export class FormControl extends AbstractControl {
  /** @internal */
  _onChange: Function[] = [];
 ...
 /**
  * Register a listener for change events.
  */
 registerOnChange(fn: Function): void { this._onChange.push(fn); }
}

如今咱們來回顧一下 setValue() 的相關知識點。對於 FormGroup 對象,咱們能夠經過 setValue() 方法更新表單的值,具體使用示例以下:

this.form.setValue({
  name: 'Semlinker',
  event: {
    title: 'Angular 4.x\'s Road',
    location: 'Xiamen'
  }
});

以上代碼成功運行後,咱們就能成功更新表單的值。但若是咱們使用下面的方式,就會拋出異常:

this.form.setValue({
  $exists: function () {},
  $key: '-KWihhw-f1kw-ULPG1ei',
  name: 'Semlinker',
  event: {
    title: 'Angular 4.x\'s Road',
    location: 'Xiamen'
  }
});

最後咱們來總結一下 FormGroupFormControl 類中 patchValue() 與 setValue() 的區別。

patchValue vs setValue

FormControl

patchValue

patchValue(value: any, options: {
    onlySelf?: boolean,
    emitEvent?: boolean,
    emitModelToViewChange?: boolean,
    emitViewToModelChange?: boolean
  } = {}): void {
    this.setValue(value, options);
}

setValue

setValue(value: any,
  {onlySelf, emitEvent, emitModelToViewChange, emitViewToModelChange}: {
    onlySelf?: boolean,
    emitEvent?: boolean,
    emitModelToViewChange?: boolean,
    emitViewToModelChange?: boolean
  } = {}): void {
    this._value = value;
    if (this._onChange.length && emitModelToViewChange !== false) {
      this._onChange.forEach((changeFn) => changeFn(this._value,
          emitViewToModelChange !== false));
    }
    this.updateValueAndValidity({onlySelf, emitEvent});
}

經過源碼咱們發現對於 FormControl 對象來講,patchValue() 和 setValue() 這兩個方法是等價的。此外 setValue() 方法中作了三件事:

  • 更新控件當前值

  • 判斷是否註冊 onChange 事件,如有則循環調用已註冊的 changeFn 函數。

  • 從新計算控件的值和驗證狀態

FormGroup

patchValue

patchValue(
  value: {[key: string]: any},
    {onlySelf, emitEvent}: {onlySelf?: boolean, emitEvent?: boolean} = {}): void {
    Object.keys(value).forEach(name => {
      if (this.controls[name]) {
        this.controls[name].patchValue(value[name], {onlySelf: true, emitEvent});
      }
    });
    this.updateValueAndValidity({onlySelf, emitEvent});
}

setValue

setValue(
  value: {[key: string]: any},
   {onlySelf, emitEvent}: {onlySelf?: boolean, emitEvent?: boolean} = {}): void {
    this._checkAllValuesPresent(value); // 判斷的是否爲全部控件都設置更新值
    Object.keys(value).forEach(name => {
      this._throwIfControlMissing(name); // 判斷控件是否存在
      this.controls[name].setValue(value[name], {onlySelf: true, emitEvent});
    });
    this.updateValueAndValidity({onlySelf, emitEvent}); // 從新計算控件的值和驗證狀態
}

經過查看源碼,咱們發現 setValue() 方法相比 patchValue() 會更嚴格,會執行多個判斷:

  • 判斷的是否爲全部控件都設置更新值

  • 判斷控件是否存在

而 patchValue() 方法,會先使用 this.controls[name] 進行過濾,只更新參數 value 中設定控件的值。

我有話說

爲何 FormControl 中 patchValue() 和 setValue() 是等價的,還須要兩個方法?

由於 FormControl 繼承於 AbstractControl 抽象類:

export class FormControl extends AbstractControl { }

AbstractControl 抽象類中定義了 patchValue() 和 setValue() 兩個抽象方法,須要由子類實現:

/**
 * Sets the value of the control. Abstract method (implemented in sub-classes).
*/
abstract setValue(value: any, options?: Object): void;

/**
 * Patches the value of the control. Abstract method (implemented in sub-classes).
 */
abstract patchValue(value: any, options?: Object): void;

建立 FormControl 控件有哪些常見的方式?

方式一

const ctrl = new FormControl('some value');
console.log(ctrl.value); // 'some value'

方式二

const ctrl = new FormControl({ value: 'n/a', disabled: true });
console.log(ctrl.value); // 'n/a'
console.log(ctrl.status); // DISABLED

若沒有設置 disabled 屬性,即:

const ctrl = new FormControl({ value: 'n/a'});
console.log(ctrl.value); // Object {value: "n/a"}
console.log(ctrl.status); // VALID

爲何呢?由於內部在初始設置控件狀態時,會對傳入的 formState 參數進行判斷:

FormControl 構造函數

constructor(
      formState: any = null, 
      validator: ValidatorFn|ValidatorFn[] = null,
      asyncValidator: AsyncValidatorFn|AsyncValidatorFn[] = null) {
    ...
    this._applyFormState(formState);
    ...
}

_applyFormState() 方法

private _applyFormState(formState: any) {
    if (this._isBoxedValue(formState)) {
      this._value = formState.value;
      formState.disabled ? this.disable({onlySelf: true, emitEvent: false}) :
                           this.enable({onlySelf: true, emitEvent: false});
    } else {
      this._value = formState;
    }
}

_isBoxedValue() 方法

_isBoxedValue(formState: any): boolean {
    return typeof formState === 'object' && formState !== null &&
        Object.keys(formState).length === 2 && 'value' in formState && 
              'disabled' in formState;
}

方式三

const ctrl = new FormControl('', Validators.required);
console.log(ctrl.value); // ''
console.log(ctrl.status); //INVALID

建立 FormGroup 對象有哪些常見的方式?

方式一

const form = new FormGroup({
  first: new FormControl('Nancy', Validators.minLength(2)),
  last: new FormControl('Drew'),
});

console.log(form.value);   // Object {first: "Nancy", last: "Drew"}
console.log(form.status);  // 'VALID'

方式二

const form = new FormGroup({
  password: new FormControl('', Validators.minLength(2)),
  passwordConfirm: new FormControl('', Validators.minLength(2)),
}, passwordMatchValidator);

function passwordMatchValidator(g: FormGroup) {
  return g.get('password').value === g.get('passwordConfirm').value
     ? null : { 'mismatch': true };
}

上面代碼中,咱們在建立 FormGroup 對象時,同時設置了同步驗證器 (validator),用於校驗 password (密碼) 和 passwordConfirm (確認密碼) 的值是否匹配。

參考資源

相關文章
相關標籤/搜索