ControlValueAccessor
ControlValueAccessor 是一个连接表单模型和视图(DOM元素)的接口,自定义的表单控件必须实现这个接口,它的作用是:
- 把 form 模型中值映射到视图中
- 当视图发生变化时,通知 form directives 或 form controls
Angular 引入这个接口的原因是,不同的输入控件数据更新方式是不一样的。例如,对于我们常用的文本输入框来说,我们是设置它的 value 值,而对于复选框 (checkbox) 我们是设置它的 checked 属性。实际上,不同类型的输入控件都有一个 ControlValueAccessor,用来更新视图
Angular 中常见的 ControlValueAccessor 有:
- DefaultValueAccessor - 用于 text 和 textarea 类型的输入控件
- SelectControlValueAccessor - 用于 select 选择控件
- CheckboxControlValueAccessor - 用于 checkbox 复选控件
实现ControlValueAccessor接口
首先我们先看一下 ControlValueAccessor 接口,具体如下:
export interface ControlValueAccessor {
writeValue(obj: any): void;
registerOnChange(fn: any): void;
registerOnTouched(fn: any): void;
setDisabledState?(isDisabled: boolean): void;
}
- writeValue(obj: any):该方法用于将模型中的新值写入视图或 DOM 属性中,即model->view
- registerOnChange(fn: any):设置当控件接收到 change 事件后,调用的函数,可以用来通知外部,组件已经发生变化,即view->model
- registerOnTouched(fn: any):设置当控件接收到 touched 事件后,调用的函数
- setDisabledState?(isDisabled: boolean):当控件状态变成 DISABLED 或从 DISABLED 状态变化成 ENABLE 状态时,会调用该函数。该函数会根据参数值,启用或禁用指定的 DOM 元素。
当表单初始化的时候,将会使用表单模型中对应的初始值作为参数,调用 writeValue() 方法
<form #form="ngForm">
<exe-counter name="counter" ngModel></exe-counter>
<button type="submit">Submit</button>
</form>
你会发现,我们没有为 CounterComponent 组件设置初始值,因此我们要调整一下 writeValue() 中的代码,具体如下:
writeValue(value: any) {
if (value) {
this.count = value;//接收从表单模型层传进来的数据
}
}
现在,只有当合法值 (非 undefined、null、"") 写入控件时,它才会覆盖默认值。接下来,我们来实现 registerOnChange() 和 registerOnTouched() 方法。registerOnChange() 可以用来通知外部,组件已经发生变化。registerOnChange() 方法接收一个 fn 参数,用于设置当控件接收到 change 事件后,调用的函数。而对于 registerOnTouched() 方法,它也支持一个 fn 参数,用于设置当控件接收到 touched 事件后,调用的函数。示例中我们不打算处理 touched 事件,因此 registerOnTouched() 我们设置为一个空函数。具体如下:
@Component(...)
class CounterComponent implements ControlValueAccessor {
...
propagateChange = (_: any) => {};
registerOnChange(fn: any) {
this.propagateChange = fn;//每次控件view层的值发生改变,都要调用该方法通知外部
}
registerOnTouched(fn: any) {}
}
注册成为表单控件
- NG_VALUE_ACCESSOR:token类型为ControlValueAccessor,将控件本身注册到DI框架成为一个可以让表单访问其值的控件
- NG_VALIDATORS:将控件注册成为一个可以让表单得到其验证状态的控件,NG_VALIDATORS的token类型为function或Validator,配合useExisting,可以让控件只暴露对应的function或Validator的validate方法。针对token为Validator类型来说,控件实现了validate方法就可以实现表单控件验证
- forwardRef:向前引用,允许我们引用一个尚未定义的对象
- multi:设为true,表示这个token对应多个依赖项,使用相同的token去获取依赖项的时候,获取的是已注册的依赖对象列表。如果不设置multi为true,那么对于相同token的提供商来说,后定义的提供商会覆盖前面已定义的提供商
注册表单控件:
步骤一:创建Token为NG_VALUE_ACCESSOR的提供商
@Component({
selector: 'exe-counter',
...
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => CounterComponent ),
multi: true
}
]
})
步骤二:创建Token为NG_VALIDATORS的表单控件验证器
@Component({
selector: 'exe-counter',
...
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => CounterComponent ),
multi: true
},
{
provide: NG_VALIDATORS,
useValue: validateCounterRange,
multi: true
}
]
CounterComponent 组件的完整代码如下:
import { Component, Input, forwardRef } from '@angular/core';
import {
ControlValueAccessor, NG_VALUE_ACCESSOR, NG_VALIDATORS,
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 validateCounterRange: ValidatorFn = (control: AbstractControl):
ValidationErrors => {
return (control.value > 10 || control.value < 0) ?
{ 'rangeError': { current: control.value, max: 10, min: 0 } } : null;
};
export const EXE_COUNTER_VALIDATOR = {
provide: NG_VALIDATORS,
useValue: validateCounterRange,
multi: true
};
@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 {
@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) {
this.count = value;
}
}
registerOnChange(fn: any) {
this.propagateChange = fn;
}
registerOnTouched(fn: any) { }
increment() {
this.count++;
}
decrement() {
this.count--;
}
}