angular的ControlValueAccessor是一个连接表单模型和视图DOM的抽象类接口

使自定义表单组件像原生input一样映射到form表单模型中, 拥有自定义表单组件的form也能使用响应式表单. (也就是使自定义表单组件拥有formControlName属性和ngModel接口.)

毕竟响应式表单才是angular的利器.

ControlValueAccessor

export interface ControlValueAccessor {
  writeValue(obj: any): void;
  registerOnChange(fn: any): void;
  registerOnTouched(fn: any): void;
  setDisabledState?(isDisabled: boolean): void;
}

writeValue(obj: any):

该方法是接收模版中的ngModel.

writeValue(value: any): void {
 this._renderer.setProperty(this._elementRef.nativeElement, 'value', value);
}

registerOnChange(fn: any): void:

该方法是组件接收到 change 事件的回调, 可以用来通知外部达成双向绑定, 即ngModelChange.

registerOnChange(fn: (_: any) => void): void {
 this._onChange = fn;
}

registerOnTouched(fn: any):

接收到 touched 事件的回调.

registerOnTouched(fn: any): void {
  this._onTouched = fn;
}

setDisabledState?(isDisabled: boolean):

该方法是组件输入状态 disable <=> enable 变化时的回调。该方法会根据参数值,启用或禁用指定的DOM元素.

以下组件类实现了ControlValueAccessor接口.

CheckboxControlValueAccessor

用于checkbox复选组件 选择器:

  • input[type=checkbox][formControlName]
  • input[type=checkbox][formControl]
  • input[type=checkbox][ngModel]

NumberValueAccessor

用于number类型的输入组件 选择器:

  • input[type=number][formControlName]
  • input[type=number][formControl]
  • input[type=number][ngModel]

DefaultValueAccessor

用于 text 和 textarea 类型的输入组件 选择器:

  • input:not([type=checkbox])[formControlName]
  • textarea[formControlName]
  • input:not([type=checkbox])[formControl]
  • textarea[formControl]
  • input:not([type=checkbox])[ngModel]
  • textarea[ngModel]
  • [ngDefaultControl]

RadioControlValueAccessor

用于radio单选组件 选择器:

  • input[type=radio][formControlName]
  • input[type=radio][formControl]
  • input[type=radio][ngModel]

扩展方法:

  • fireUncheck(value: any): void:取消选中的回调.

RangeValueAccessor

用于范围输入组件 选择器:

  • input[type=range][formControlName]
  • input[type=range][formControl]
  • input[type=range][ngModel]

SelectControlValueAccessor

用于select组件 选择器:

  • select:not([multiple])[formControlName]
  • select:not([multiple])[formControl]
  • select:not([multiple])[ngModel]

扩展方法:

  • compareWith: (o1: any, o2: any) => boolean:比较函数. 例如option的ngValue是一个对象, 当选中项填入表单时,需要编写一个比较函数来处理当前选中的对象是哪一个option.

SelectMultipleControlValueAccessor

用于多选select组件 选择器:

  • select[multiple][formControlName]
  • select[multiple][formControl]
  • select[multiple][ngModel]

扩展方法: compareWith: (o1: any, o2: any) => boolean:比较函数. 例如option的ngValue是一个对象, 当选中项填入表单时, 需要编写一个比较函数来处理当前选中的对象是哪一个option.

EG

自定义表单组件代码结构

import {
  Component,
  OnInit,
  HostListener,
  ViewEncapsulation,
  forwardRef,
  Input,
  OnDestroy,
  ChangeDetectorRef,
  ChangeDetectionStrategy
} from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { Subject } from 'rxjs';

@Component({
  selector       : '[app-radiobox]',
  templateUrl    : './radiobox.component.html',
  styleUrls      : ['./radiobox.component.styl'],
  encapsulation  : ViewEncapsulation.None,
  changeDetection: ChangeDetectionStrategy.OnPush,
  host           : {
    '[class.radio-wrapper]'        : 'true',
    '[class.radio-wrapper-checked]': 'checked'
  },
  providers: [
    {
      provide    : NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => RadioboxComponent),
      multi      : true
    }
  ]
})
export class RadioboxComponent implements OnInit, OnDestroy, ControlValueAccessor {
  @Input()
  value: boolean;
  checked: boolean;
  select$ = new Subject<RadioboxComponent>();

  onChange: (_: any) => void = () => null;
  onTouched: () => void = () => null;

  constructor(private _cdr: ChangeDetectorRef) {}

  @HostListener('click', ['$event'])
  onClick(e: MouseEvent): void {
    e.stopPropagation();
    e.preventDefault();
    this.checked = !this.checked;
    this.onChange(this.checked);
    this.select$.next(this);
  }

  writeValue(value: boolean) {
    this.checked = value;
  }
  registerOnChange(fn: (_: boolean) => {}): void {
    this.onChange = fn;
  }
  registerOnTouched(fn: () => {}): void {
    this.onTouched = fn;
  }

  ngOnInit() {}

  ngOnDestroy() {}
}

调用

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

@Component({
  selector: 'app-example',
  template: `
    <form [formGroup]="testForm">
      <label>试试</label>
      <label app-radiobox formControlName="check">check me</label>
    </form>
  `
})
export class ExampleComponent implements OnInit {
  testForm: FormGroup = this._fb.group({
    check: false
  });
  constructor(private _fb: FormBuilder) {}

  ngOnInit() {
    this.testForm.valueChanges.subscribe(d => {
      console.log(d);
    });
  }
}