自定义表单

470 阅读2分钟

angular 表单

首先,只要你创建表单,Angular就会创建对应FormControl,无论是模板驱动表单还是响应式表单。模板驱动表单的FormControl是由NgModel指令隐性创建,而响应式表单是由你自己创建,通过FormControlName指令将Angular表单元素和原生表单元素进行绑定。

也就是说在Angular中的表单,不是原生表单,而是封装后的Angular表单。不仅仅是原生的表单控件可以处理封装成Angular表单,其他自定义的Angular组件也可以,只要定义了ControlValueAccessor。

ControlValueAccessor

那ControlValueAccessor是什么呢?它是原生元素和Angular表单之间的桥梁,将组件或者指令继承ControlValueAccessor的接口就能变成Angular表单使用了。

image.png

interface ControlValueAccessor {
    writeValue(obj: any): void
    registerOnChange(fn: any): void
    registerOnTouched(fn: any): void
    setDisabledState?(isDisabled: boolean): void
}
  • writeValue(obj: any):该方法用于将模型中的新值写入视图或 DOM 属性中。
  • registerOnChange(fn: any):设置当控件接收到 change 事件后,调用的函数
  • registerOnTouched(fn: any):设置当控件接收到 touched 事件后,调用的函数
  • setDisabledState?(isDisabled: boolean):当控件状态变成 DISABLED 或从 DISABLED 状态变化成 ENABLE 状态时,会调用该函数。该函数会根据参数值,启用或禁用指定的 DOM 元素。

原生表单控件和Angular表单控件保持一致的原理

我们看下formControl指令的实现:

// packages/forms/src/directives/reactive_directives/form_control_directive.ts

export class FormControlDirective ... {
  ...
  ngOnChanges(changes: SimpleChanges): void {
    if (this._isControlChanged(changes)) {
      setUpControl(this.form, this);

formControl指令调用了setUpControl函数来实现formControl和ControlValueAccessor之间的交互。

// packages/forms/src/directives/shared.ts

export function setUpControl(control: FormControl, dir: NgControl) {
  
  // 初始化原生表单控件
  dir.valueAccessor.writeValue(control.value);
   
  // 监听原生表单控件,将值同步给form control
  dir.valueAccessor.registerOnChange((newValue: any) => {
    ...
    control.setValue(newValue, {emitModelToViewChange: false});
  });

  // 反之,监听form control,将值同步给原生表单控件
  control.registerOnChange((newValue: any, ...) => {
    dir.valueAccessor.writeValue(newValue);
  });

Demo

import {
  Component,
  Input,
  OnChanges,
  SimpleChanges,
  forwardRef,
} from "@angular/core";
import {
  ControlValueAccessor,
  NG_VALUE_ACCESSOR,
  NG_VALIDATORS,
  Validator,
  AbstractControl,
  ValidatorFn,
  ValidationErrors,
  FormControl,
} from "@angular/forms";

export const EXE_COUNTER_VALUE_ACCESSOR: any = {
  provide: NG_VALUE_ACCESSOR,
  useExisting: forwardRef(() => CounterComponent),
  multi: true,
};

export const EXE_COUNTER_VALIDATOR = {
  provide: NG_VALIDATORS,
  useExisting: forwardRef(() => CounterComponent),
  multi: true,
};

export function createCounterRangeValidator(
  maxValue: number,
  minValue: number
) {
  return (control: AbstractControl): ValidationErrors => {
    return control.value > +maxValue || control.value < +minValue
      ? { rangeError: { current: control.value, max: maxValue, min: minValue } }
      : null;
  };
}

@Component({
  selector: "exe-counter",
  template: `
    <div>
      <p>当前值: {{ count }}</p>
      <button (click)="increment()">+</button>
      <button (click)="decrement()">-</button>
    </div>
  `,
  providers: [EXE_COUNTER_VALUE_ACCESSOR, EXE_COUNTER_VALIDATOR],
})
export class CounterComponent
  implements ControlValueAccessor, Validator, OnChanges
{
  @Input() _count: number = 0;

  get count() {
    return this._count;
  }

  set count(value: number) {
    this._count = value;
    this.propagateChange(this._count);
  }

  propagateChange = (_: any) => {};

  writeValue(value: any) {
    if (value !== undefined) {
      this.count = value;
    }
  }

  registerOnChange(fn: any) {
    this.propagateChange = fn;
  }

  registerOnTouched(fn: any) {}

  increment() {
    this.count++;
  }

  decrement() {
    this.count--;
  }

  private _validator: ValidatorFn;
  private _onChange: () => void;

  @Input() counterRangeMin: number; // 设置数据有效范围的最大值

  @Input() counterRangeMax: number; // 设置数据有效范围的最小值

  // 监听输入属性变化,调用内部的_createValidator()方法,创建RangeValidator
  ngOnChanges(changes: SimpleChanges): void {
    if ("counterRangeMin" in changes || "counterRangeMax" in changes) {
      this._createValidator();
    }
  }

  // 动态创建RangeValidator
  private _createValidator(): void {
    this._validator = createCounterRangeValidator(
      this.counterRangeMax,
      this.counterRangeMin
    );
  }

  // 执行控件验证
  validate(c: AbstractControl): ValidationErrors | null {
    return this.counterRangeMin == null || this.counterRangeMax == null
      ? null
      : this._validator(c);
  }
}

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

@Component({
  selector: 'exe-app',
  template: `
    <form [formGroup]="form">
      <exe-counter formControlName="counter" 
        counterRangeMin="5" 
        counterRangeMax="8">
      </exe-counter>
    </form>
    <p *ngIf="!form.valid">Counter is invalid!</p>
    <pre>{{ form.get('counter').errors | json }}</pre>
  `,
})
export class AppComponent {
  form: FormGroup;

  constructor(private fb: FormBuilder) { }

  ngOnInit() {
    this.form = this.fb.group({
      counter: 5
    });
  }
}