美文网首页angular2Angular 4.x 修仙之路
Angular 4 表单 patchValue and setV

Angular 4 表单 patchValue and setV

作者: semlinker | 来源:发表于2017-06-04 22:37 被阅读1041次

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

    参考资源

    相关文章

      网友评论

        本文标题:Angular 4 表单 patchValue and setV

        本文链接:https://www.haomeiwen.com/subject/vwpbfxtx.html