ControlValueAccessor 以下简称 CVA 定义一:将 form 模型中的值映射到视图中,当视图发生变化时通知 form 定义二:实现一种针对 angular 表单的新的数据通信机制
目标:实现自定义表单控件
学习目标:
- 自定义表单控件如何实现 CVA 接口
- CVA 在 angular 表单架构中扮演的角色
angular 表单控件中的对象/指令: formControl/formControlName
formControlName 指令能够追踪单个表单控件,实现与表单控件的交互。
而原生表单控件是有限的,有时候需要自定义表单控件,因此需要一种机制来桥接表单控件和 formControlName 指令。而这种机制就是所谓的 CVA 。
需要做的就是将组件/指令转化为 CVA 类型的对象,如何实现?
- 实现 CVA 接口,任何组件/指令都可以实现
- 注册为 NG.VALUE.ACCESSOR
详细介绍:
CVA 接口
writeValue:设置原生表单控件的值
registerOnChange:原生表单控件的值更新时触发的回调(将新值传给回调,更新 angular 表单控件的值,即 formControlName 绑定的值)
registerOnTouch:用户和控件交互时触发的回调
表单指令是如何被自定义表单接收的呢?
所有表单指令,包括 formControl/formControlName 指令,都会调用 setUpControl 函数来让表单控件和CVA 实现交互,即实现原生表单控件与angular 表单控件的数据同步。export function setUpControl(control: FormControl, dir: NgControl) {
// 调用 writeValue() 初始化表单控件值
dir.valueAccessor.writeValue(control.value);
// 设置原生控件值更新时监听器,每当原生控件值更新,Angular 表单控件值也更新
valueAccessor.registerOnChange((newValue: any) => {
control.setValue(newValue, {emitModelToViewChange: false});
});
// 设置 Angular 表单控件值更新监听器,每当 Angular 表单控件值更新,原生控件值也更新
control.registerOnChange((newValue: any, ...) => {
dir.valueAccessor.writeValue(newValue);
});
...
}
formControlName 通过绑定的方式将属性值传入表单控件,表单控件通过 writeValue 接收并赋值到自身属性。
通过一个例子更清楚用法:【解耦出表单及其相应的校验方法】
provide: 组件实现接口后,还需使 angular 接受它,angular 通过令牌注册组件/校验器等 forwardRef: angular 中的向前引用,将函数作为返回类的参数,解决类无法提升无法在声明前使用的问题。
// 引入部分忽略,只看主要代码
@Component({
selector: 'app-teacher-input',
templateUrl: './teacher-input.component.html',
styleUrls: ['./teacher-input.component.scss'],
// 注册 CVA
providers: [
{
provide: NG_VALUE_ACCESSOR,
// 在 NG_VALUE_ACCESSOR 令牌上注册我们的组件,以让CVA控制表单控件
useExisting: forwardRef(() => TeacherInputComponent),
multi: true
// 告诉 angular 是一个多提供者,即为单个令牌提供多个依赖项,获取的是依赖项列表
},
{
provide: NG_VALIDATORS,
useValue: validateTeacherName,
multi: true
},
]
})
// 实现 CVA
export class TeacherInputComponent implements ControlValueAccessor {
visible = false;
teachers: Array<{ id: string; value: string; selected: boolean }>;
cpTeachers: Array<{ id: string; value: string; selected: boolean }>;
// 增加属性访问器使代码更美观
@Input() _teacherName = '';
propagateChange = (_: any) => {};
get teacherName() {
return this._teacherName;
}
set teacherName(value: string) {
this._teacherName = value;
this.propagateChange(this._teacherName);
}
constructor() {}
// 设置原生表单控件的值
writeValue(value: any): void {
if (value !== undefined) {
this._teacherName = value;
}
}
// 原生表单控件值更新时触发回调,将新值作为参数传出去 => angular 表单控件也会更新
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(): void {}
// 对属性 teacherName 的一些操作事件
onClick($event) {
// ...
}
ngOnInit(): void {
}
}
// 声明校验函数
export function validateTeacherName(c: FormControl) {
let err = {
Error: {
given: c.value,
name: 'error'
}
};
console.log(c.value);
return c.value === '123' ? err : null;
}
<nz-form-item>
<nz-form-label [nzSpan]="4" nzRequired>反馈人</nz-form-label>
<app-teacher-input formControlName="teacherName"></app-teacher-input>
<p *ngIf="validateForm.value.teacherName === '123'">反馈人姓名存在问题,请更换</p>
</nz-form-item>
ngOnInit(): void {
this.validateForm = this.fb.group({
teacherName: ['鲁欣', [validateTeacherName]]
});
}
参考链接:
forwardRef 详解
自定义表单