Angular 4 表单 patchValue and setValue

在 Angular 4 中有多种方式可以更新表单的值,对于使用响应式表单的场景,我们可以通过框架内部提供的 API ,(如 patchValue 和 setValue )方便地更新表单的值。这篇文章我们将介绍如何使用 patchValue 和 setValue 方法更新表单的值,此外还会进一步介绍它们之间的差异。

Reactive Form Setup

app.module.ts

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.ts

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

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

event-form.component.ts

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() 方法,具体代码如下:

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

首先,Object.keys() 会返回对象 key 值的数组,例如:

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

此外 this.controls 包含了 FormGroup 对象中的所有 FormControl 控件,我们可以通过 this.controls[name] 方式,访问到 name 对应的控件对象。

现在让我们来回顾一下上面的示例中创建 FormGroup 对象的相关代码:

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

参考资源

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,793评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,567评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,342评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,825评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,814评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,680评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,033评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,687评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,175评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,668评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,775评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,419评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,020评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,206评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,092评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,510评论 2 343

推荐阅读更多精彩内容