在 Angular 4.x 中有多種方式能夠更新表單的值,對於使用響應式表單的場景,咱們能夠經過框架內部提供的 API ,(如 patchValue 和 setValue )方便地更新表單的值。這篇文章咱們將介紹如何使用 patchValue 和 setValue 方法更新表單的值,此外還會進一步介紹它們之間的差別。javascript
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() 方法,而後在介紹 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() 方法。
首先,咱們來看一下 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' } });
最後咱們來總結一下 FormGroup
和 FormControl
類中 patchValue() 與 setValue() 的區別。
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
函數。
從新計算控件的值和驗證狀態
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 繼承於 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;
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
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 (確認密碼) 的值是否匹配。