【angular开发总结】- Angular响应式表单和表单控件封装

2,341 阅读9分钟

angular的表单分响应式表单和模板驱动表单。

响应式表单比模板驱动表单更有可伸缩性。它们提供对底层表单 API 的直接访问,并且在视图和数据模型之间使用同步数据流,从而可以更轻松地创建大型表单。

模板驱动表单专注于简单的场景,可复用性没那么高。在视图和数据模型之间使用异步数据流

1、理解angular响应式表单

常用表单基础类

  • FormControl 实例用于追踪单个表单控件的值和验证状态。
  • FormGroup 用于追踪一个表单控件组的值和状态。
  • FormArray 用于追踪表单控件数组的值和状态。
  • ControlValueAccessor 用于在 Angular 的 FormControl 实例和内置 DOM 元素之间创建一个桥梁。

建立响应式表单

对于响应式表单,你可以直接在组件类中定义表单模型。[formControl] 指令会通过内部值访问器ControlValueAccessor来把显式创建的 FormControl 实例与视图中的特定表单元素联系起来。

在下面例子中,表单模型是 FormControl 实例。

import { Component } from '@angular/core';
import { FormControl } from '@angular/forms';
​
@Component({
  selector: 'app-reactive-favorite-color',
  template: `
    Favorite Color: <input type="text" [formControl]="favoriteColorControl">
  `
})
export class FavoriteColorComponent {
  favoriteColorControl = new FormControl('');
}

图 1.在响应式表单中直接访问表单模型

响应式表单中的数据流

在响应式表单中,视图中的每个表单元素都直接链接到一个表单模型(FormControl 实例)。 从视图到模型的修改以及从模型到视图的修改都是同步的,而且不依赖于 UI 的渲染方式。

视图=>模型 的数据流步骤:

  1. 最终用户在输入框元素中键入了一个值,这里是 "Blue"。
  2. 这个输入框元素会发出一个带有最新值的 "input" 事件。
  3. 这个控件值访问器 ControlValueAccessor 会监听表单输入框元素上的事件,并立即把新值传给 FormControl 实例。
  4. FormControl 实例会通过 valueChanges 这个可观察对象发出这个新值。
  5. valueChanges 的任何一个订阅者都会收到这个新值。

模型=>视图 的数据流步骤:

  1. favoriteColorControl.setValue() 方法被调用,它会更新这个 FormControl 的值。
  2. FormControl 实例会通过 valueChanges 这个可观察对象发出新值。
  3. valueChanges 的任何订阅者都会收到这个新值。
  4. 该表单输入框元素上的控件值访问器ControlValueAccessor会把控件更新为这个新值。

响应式表单实现原理

响应式表单将formControl实例挂载到formControl指令或者formControlName指令上,两种指令再通过内部的值访问器ControlValueAccessorFormControl 实例与视图中的特定表单元素联系起来。

Angular 为所有原生 DOM 表单元素创建了 Angular 表单控件

AccessorForm Element
DefaultValueAccessorinput,textarea
CheckboxControlValueAccessorinput[type=checkbox]
NumberValueAccessorinput[type=number]
RadioControlValueAccessorinput[type=radio]
RangeValueAccessorinput[type=range]
SelectControlValueAccessorselect
SelectMultipleControlValueAccessorselect[multiple]

从上表中可看到,当 Angular 在组件模板中中遇到 inputtextarea DOM 原生控件时,会使用DefaultValueAccessor 指令。

1 源码分析
  • formControl指令

    实例化时,初始化ControlValueAccessor,调用 setUpControl() 函数

    // form_control_directive.ts
    export class FormControlDirective extends NgControl implements OnChanges {
      ...
      constructor(
          ...
          @Optional() @Self() @Inject(NG_VALUE_ACCESSOR) valueAccessors: ControlValueAccessor[],
      ) {
        ...
        this.valueAccessor = selectValueAccessor(this, valueAccessors);
      }
    ​
      /** @nodoc */
      ngOnChanges(changes: SimpleChanges): void {
        if (this._isControlChanged(changes)) {
          setUpControl(this.form, this);
          ....
        }
      }
    }
    ​
    
  • formControlName指令

    实例化时,初始化ControlValueAccessor,调用formGroup指令的addControl()addControl方法中再调用setUpControl() 函数。

    // form_control_name.ts
    export class FormControlName extends NgControl implements OnChanges, OnDestroy {
      ...
      constructor(
          ...
          @Optional() @Self() @Inject(NG_VALUE_ACCESSOR) valueAccessors: ControlValueAccessor[],) {
        this.valueAccessor = selectValueAccessor(this, valueAccessors);
      }
    ​
      /** @nodoc */
      ngOnChanges(changes: SimpleChanges) {
        if (!this._added) this._setUpControl();
        ...
      }
    ​
      private _setUpControl() {
        ...
        // 调用formGroup指令里的addControl()
        (this as {control: FormControl}).control = this.formDirective.addControl(this);
        ...
        this._added = true;
      }
    }
    
  • formGroup指令

    // form_group_directive.ts
    export class FormGroupDirective ... {
      ...
      /**
       * @description
       * Method that sets up the control directive in this group, re-calculates its value
       * and validity, and adds the instance to the internal list of directives.
       *
       * @param dir The `FormControlName` directive instance.
       */
      addControl(dir: FormControlName): FormControl {
        ...
        setUpControl(ctrl, dir);
        ...
        return ctrl;
      }
    }
    
  • setUpControl()

    为formControl实例注册事件监听,实现原生表单控件和 Angular 表单控件的数据同步。

    //shared.ts
    // 为formControl实例注册事件监听
    export function setUpControl(control: FormControl, dir: NgControl): void {
      ...
      // 调用 writeValue() 初始化视图表单控件值
      dir.valueAccessor!.writeValue(control.value);
    ​
      // 注册视图改变的监听事件
      setUpViewChangePipeline(control, dir);
        
      // 注册表单控件值更新监听事件
      setUpModelChangePipeline(control, dir);
    ​
      // 注册视图失焦事件
      setUpBlurPipeline(control, dir);
    }
    ​
    // 原生控件值更新时监听器,每当原生控件值更新,Angular 表单控件值也更新 视图 => 模型
    function setUpViewChangePipeline(control: FormControl, dir: NgControl): void {
      dir.valueAccessor!.registerOnChange((newValue: any) => {
        ...
        if (control.updateOn === 'change') updateControl(control, dir);
      });
    }
    ​
    // 原生控件失焦,Angular 表单控件值也更新 视图 => 模型
    function setUpBlurPipeline(control: FormControl, dir: NgControl): void {
      dir.valueAccessor!.registerOnTouched(() => {
        ...
        if (control.updateOn === 'blur' && control._pendingChange) updateControl(control, dir);
      });
    }
        
    // 更新formcontrol实例值
    function updateControl(control: FormControl, dir: NgControl): void {
      ...
      control.setValue(control._pendingValue, {emitModelToViewChange: false});
    }
    ​
    // 设置原生控件值更新时监听器,每当原生控件值更新,Angular 表单控件值也更新  模型 => 视图
    function setUpModelChangePipeline(control: FormControl, dir: NgControl): void {
      control.registerOnChange((newValue: any, emitModelEvent: boolean) => {
        // control -> view
        dir.valueAccessor!.writeValue(newValue);
    ​
        // control -> ngModel
        if (emitModelEvent) dir.viewToModelUpdate(newValue);
      });
    }
    
  • FormControl实例

    export class FormControl extends AbstractControl {
        // 控件值改变事件
        _onChange: Function[] = [];
        
        // 更新控件值
        setValue(value: any, options: {
            onlySelf?: boolean,
            emitEvent?: boolean,
            emitModelToViewChange?: boolean,
            emitViewToModelChange?: boolean
          } = {}): void {
            (this as {value: any}).value = this._pendingValue = value;
            if (this._onChange.length && options.emitModelToViewChange !== false) {
              this._onChange.forEach(
                  (changeFn) => changeFn(this.value, options.emitViewToModelChange !==                              false));
            }
            
            // 更新值和校验
            this.updateValueAndValidity(options);
        }
        
        /**
         * Register a listener for change events.
         *
         * @param fn The method that is called when the value changes
         */
        registerOnChange(fn: Function): void {
          this._onChange.push(fn);
        }
    }
    
    /**
     *重新计算控件的值和验证状态。
     *
     *默认情况下,它还更新其祖先的值和有效性。
     *
     * @param opts配置选项确定控件传播更改和发出事件的方式
     *应用更新和有效性检查后。
     * * `onlyself`:为true时,仅更新此控件。当为假或未提供时,
     *更新所有直接祖先。默认值为false。
     * * `emitEvent`:当提供true或未提供(默认值)时,`statusChanges`和`值更改'
     *
     *当控件更新时,可观察对象会发出具有最新状态和值的事件。
     *为false时,不发出任何事件。
    */
    updateValueAndValidity(
    	opts: {onlySelf?: boolean, 
        emitEvent?: boolean} = {}
    ): void {
        this._setInitialStatus();
        this._updateValue();  // 更新value,如果外面调用disable(),会从value中去掉该项
    
        if (this.enabled) {
          this._cancelExistingSubscription();
          (this as {errors: ValidationErrors | null}).errors = this._runValidator();  // 生成校验信息
          (this as {status: string}).status = this._calculateStatus();
    
          if (this.status === VALID || this.status === PENDING) {
            this._runAsyncValidator(opts.emitEvent);
          }
        }
    
        if (opts.emitEvent !== false) {
          (this.valueChanges as EventEmitter<any>).emit(this.value);  // 发出新值
          (this.statusChanges as EventEmitter<string>).emit(this.status);
        }
    
        if (this._parent && !opts.onlySelf) {
          this._parent.updateValueAndValidity(opts);  // 重新计算formGroup的值和验证状态。
        }
      }
    

通过源码分析,angular响应式表单实现原理是:

表单控件formControl实例化的时候,也就是formControlName指令初始化的时候,执行了两个操作:

一,调用formControl实例的registerOnChange()函数,将值访问器ControlValueAccessor更新表单DOM 视图的方法writeValue()注册在formControl实例的_onChange事件列表中;

二,调用值访问器ControlValueAccessor的registerOnChange()方法,将formControl实例更新表单模型值 的方法setValue()与值访问器的onChange()绑定;

表单DOM值改变 => 触发值访问器onChange() => 触发控件setValue() => 更新表单控件值

表单控件值改变 => 遍历控件_onChange => 触发值访问器writeValue() => 更新表单DOM值

2 响应式表单原理图

响应式表单.png

视图 => 模型:

input输入改变,触发ControlValueAccessor值访问器onChange()方法,在钩子函数registerOnChange()中,onChange()与回调函数fn()绑定,fn()是指令实例化的时候调用setUpControl()函数注册事件时候的回调。fn()调用updateControl()updateControl()中会执行control.setValue()从而更新FormControl 实例的值。

模型 => 视图:

control.setValue()更新表单控件值,然后遍历control.registerOnChange()注册的事件列表_onChange,该事件列表中注册了值访问器的writeValue()钩子,执行writeValue()就会更新DOM控件的值。

2、 如何新建一个表单(FormGroup、FormArray、FormBuilder)

  • FormGroup

    import { Component } from '@angular/core';
    import { FormGroup, FormControl } from '@angular/forms';
    ​
    @Component({
      selector: 'app-profile-editor',
      templateUrl: './profile-editor.component.html',
      styleUrls: ['./profile-editor.component.css']
    })
    export class ProfileEditorComponent {
      profileForm = new FormGroup({
        firstName: new FormControl(''),
        lastName: new FormControl(''),
        address: new FormGroup({
          street: new FormControl(''),
          city: new FormControl(''),
          state: new FormControl(''),
          zip: new FormControl('')
        })
      });
    }
    
  • FormArray

    适用于创建动态表单,管理任意数量的匿名控件。不需要为每个控件定义一个名字作为 key,因此,如果事先不知道子控件的数量,可选择FormArray创建表单。

    定义 FormArray 控件

    你可以通过把一组(从零项到多项)控件定义在一个数组中来初始化一个 FormArray

    profileForm = this.fb.group({
      firstName: ['', Validators.required],
      lastName: [''],
      address: this.fb.group({
        street: [''],
        city: [''],
        state: [''],
        zip: ['']
      }),
      aliases: this.fb.array([
        this.fb.control('')
      ])
    });
    

    FormGroup 中的这个 aliases 控件现在管理着一个控件,将来还可以动态添加多个。

    访问 FormArray 控件

    通过 getter 来访问控件很方便,这种方法还能很容易地重复处理更多控件。

    get aliases() {
      return this.profileForm.get('aliases') as FormArray;
    }
    

    动态添加控件

    addAlias() {
      this.aliases.push(this.fb.control(''));
    }
    
  • FormBuilder

    FormBuilder 服务有三个方法:control()group()array()。这些方法都是工厂方法,用于在组件类中分别生成 FormControlFormGroupFormArray

    import { Component } from '@angular/core';
    import { FormBuilder } from '@angular/forms';
    ​
    @Component({
      selector: 'app-profile-editor',
      templateUrl: './profile-editor.component.html',
      styleUrls: ['./profile-editor.component.css']
    })
    export class ProfileEditorComponent {
      profileForm = this.fb.group({
        firstName: [''],
        lastName: [''],
        address: this.fb.group({
          street: [''],
          city: [''],
          state: [''],
          zip: ['']
        }),
      });
    ​
      constructor(private fb: FormBuilder) { }
    }
    

3、自定义表单验证器

ngOnInit(): void {
  this.heroForm = new FormGroup({
    name: new FormControl(this.hero.name, [
      forbiddenNameValidator(/bob/i) // <-- Here's how you pass in the custom validator.
    ]),
  });
​
}
​
get name() { return this.heroForm.get('name'); }
​
export function forbiddenNameValidator(nameRe: RegExp): ValidatorFn {
  return (control: AbstractControl): ValidationErrors | null => {
    const forbidden = nameRe.test(control.value);
    return forbidden ? {forbiddenName: {value: control.value}} : null;
  };
}

4、交叉验证

创建表单模型时,把一个新的验证器传给FormGroup的第二个参数。

const heroForm = new FormGroup({
  'name': new FormControl(),
  'alterEgo': new FormControl(),
  'power': new FormControl()
}, { validators: identityRevealedValidator });
​
export const identityRevealedValidator: ValidatorFn = (control: AbstractControl):   ValidationErrors | null => {
  const name = control.get('name');
  const alterEgo = control.get('alterEgo');
​
  return name && alterEgo && name.value === alterEgo.value ? { identityRevealed: true } : null;
};

5、如何封装表单控件

  • 封装表单控件的有什么好处?

    1、表单是由各种控件组合在一起的,封装表单控件,可以将复杂的表单拆解为不同的控件,表单需要什么控件就引入相应的控件,这样表单功能容易扩充,在业务多变性的情况下,表单控件可以让表单更灵活。

    2、表单控件是一个单独的组件,可复用;表单控件可以是一个表单项,还可以是一个表单,将复杂的表单拆解为控件,有利于开发和维护。

  • 封装表单控件注意事项

    1、必须为表单控件提供值访问器ControlValueAccessor。必须将表单控件加入到验证器集合NG_VALIDATORS,这样控件的校验才会绑定到表单校验。

    2、必须实现ControlValueAccessor类和Validator接口。

例:封装的组件(表单控件missionControl)是一个表单

import { Component, forwardRef, Input, OnInit } from '@angular/core';
import {
  AbstractControl,
  ControlValueAccessor,
  FormBuilder,
  FormControl,
  FormGroup,
  NG_VALIDATORS,
  NG_VALUE_ACCESSOR,
  ValidationErrors,
  Validator,
} from '@angular/forms';
​
@Component({
  selector: 'app-mpi-mode-control',
  templateUrl: './mpi-mode-control.component.html',
  styleUrls: ['./mpi-mode-control.component.scss'],
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => MpiModeControlComponent),
      multi: true,
    },
    {
      provide: NG_VALIDATORS,
      useExisting: forwardRef(() => MpiModeControlComponent),
      multi: true,
    },
  ],
})// 需要实现ControlValueAccessor, Validator
export class MpiModeControlComponent implements OnInit, 
    ControlValueAccessor, Validator {
  
  formGroup: FormGroup;
  private propagateChange = (_: any) => {};
  private propagateTunched = (_: any) => {};
​
  constructor(
    private fb: FormBuilder,
    private customValidatorsService: CustomValidatorsService
  ) {
    this.formGroupConfig();
    this.getFormGroupState();
  }
​
  // 更新视图
  writeValue(mpiRunFormData: TMpiRunFormInfo) {
    ...
    this.formGroup.patchValue(mpiRunFormData);
  }
  
  // 视图控件change事件,更新表单控件值
  registerOnChange(fn: any): void {
    this.propagateChange = fn;
  }
​
  // 视图控件blue事件,更新表单控件值
  registerOnTouched(fn: any): void {
    this.propagateTunched = fn;
  }
​
  // 将控件校验添加到表单校验
  validate(control: AbstractControl): ValidationErrors {
    return this.formGroup?.valid
      ? null
      : { missionHpcCreateControl: { valid: false } }; // 可以为任意对象,比如{ valid:false },返回值为control.errors,详见源码updateValueAndValidity()方法
  }
​
  /**
   * 设置响应式表单
   */
  private formGroupConfig() {
    this.formGroup = this.fb.group(
      {
        mpiOnly: [false],
        shareDirectory: [
          '',
          [
            this.customValidatorsService.checkEmpty(),
            this.customValidatorsService.pathValidator()
          ],
        ],
        systemPerformance: this.fb.group({
          system: [false],
        }),
      },
      { validators: this.textValidator() }
    );
  }
​
  // 获取表单状态
  private getFormGroupState() {
    this.formGroup.valueChanges.subscribe((valuesAndVaild) => {
      ...
      this.propagateChange(valuesAndVaild);
    });
  }
}
​
<div [formGroup]="formGroup">
    <app-mpi-mode-control formControlName="missionControl">
    </app-mpi-mode-control>
</div>

参考blog