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'],// 注册 CVAproviders: [{provide: NG_VALUE_ACCESSOR,// 在 NG_VALUE_ACCESSOR 令牌上注册我们的组件,以让CVA控制表单控件useExisting: forwardRef(() => TeacherInputComponent),multi: true// 告诉 angular 是一个多提供者,即为单个令牌提供多个依赖项,获取的是依赖项列表},{provide: NG_VALIDATORS,useValue: validateTeacherName,multi: true},]})// 实现 CVAexport 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 详解
自定义表单
