在 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 (确认密码) 的值是否匹配。