ControlValueAccessor 以下简称 CVA 定义一:将 form 模型中的值映射到视图中,当视图发生变化时通知 form 定义二:实现一种针对 angular 表单的新的数据通信机制

    目标:实现自定义表单控件
    学习目标:

    • 自定义表单控件如何实现 CVA 接口
    • CVA 在 angular 表单架构中扮演的角色

    angular 表单控件中的对象/指令: formControl/formControlName
    formControlName 指令能够追踪单个表单控件,实现与表单控件的交互。
    而原生表单控件是有限的,有时候需要自定义表单控件,因此需要一种机制来桥接表单控件和 formControlName 指令。而这种机制就是所谓的 CVA 。
    需要做的就是将组件/指令转化为 CVA 类型的对象,如何实现?

    1. 实现 CVA 接口,任何组件/指令都可以实现
    2. 注册为 NG.VALUE.ACCESSOR

    详细介绍:

    • CVA 接口

      1. writeValue:设置原生表单控件的值
      2. registerOnChange:原生表单控件的值更新时触发的回调(将新值传给回调,更新 angular 表单控件的值,即 formControlName 绑定的值)
      3. registerOnTouch:用户和控件交互时触发的回调

      表单指令是如何被自定义表单接收的呢?
      所有表单指令,包括 formControl/formControlName 指令,都会调用 setUpControl 函数来让表单控件和CVA 实现交互,即实现原生表单控件与angular 表单控件的数据同步。

      1. export function setUpControl(control: FormControl, dir: NgControl) {
      2. // 调用 writeValue() 初始化表单控件值
      3. dir.valueAccessor.writeValue(control.value);
      4. // 设置原生控件值更新时监听器,每当原生控件值更新,Angular 表单控件值也更新
      5. valueAccessor.registerOnChange((newValue: any) => {
      6. control.setValue(newValue, {emitModelToViewChange: false});
      7. });
      8. // 设置 Angular 表单控件值更新监听器,每当 Angular 表单控件值更新,原生控件值也更新
      9. control.registerOnChange((newValue: any, ...) => {
      10. dir.valueAccessor.writeValue(newValue);
      11. });
      12. ...
      13. }

      formControlName 通过绑定的方式将属性值传入表单控件,表单控件通过 writeValue 接收并赋值到自身属性。


    通过一个例子更清楚用法:【解耦出表单及其相应的校验方法】

    provide: 组件实现接口后,还需使 angular 接受它,angular 通过令牌注册组件/校验器等 forwardRef: angular 中的向前引用,将函数作为返回类的参数,解决类无法提升无法在声明前使用的问题。

    1. // 引入部分忽略,只看主要代码
    2. @Component({
    3. selector: 'app-teacher-input',
    4. templateUrl: './teacher-input.component.html',
    5. styleUrls: ['./teacher-input.component.scss'],
    6. // 注册 CVA
    7. providers: [
    8. {
    9. provide: NG_VALUE_ACCESSOR,
    10. // 在 NG_VALUE_ACCESSOR 令牌上注册我们的组件,以让CVA控制表单控件
    11. useExisting: forwardRef(() => TeacherInputComponent),
    12. multi: true
    13. // 告诉 angular 是一个多提供者,即为单个令牌提供多个依赖项,获取的是依赖项列表
    14. },
    15. {
    16. provide: NG_VALIDATORS,
    17. useValue: validateTeacherName,
    18. multi: true
    19. },
    20. ]
    21. })
    22. // 实现 CVA
    23. export class TeacherInputComponent implements ControlValueAccessor {
    24. visible = false;
    25. teachers: Array<{ id: string; value: string; selected: boolean }>;
    26. cpTeachers: Array<{ id: string; value: string; selected: boolean }>;
    27. // 增加属性访问器使代码更美观
    28. @Input() _teacherName = '';
    29. propagateChange = (_: any) => {};
    30. get teacherName() {
    31. return this._teacherName;
    32. }
    33. set teacherName(value: string) {
    34. this._teacherName = value;
    35. this.propagateChange(this._teacherName);
    36. }
    37. constructor() {}
    38. // 设置原生表单控件的值
    39. writeValue(value: any): void {
    40. if (value !== undefined) {
    41. this._teacherName = value;
    42. }
    43. }
    44. // 原生表单控件值更新时触发回调,将新值作为参数传出去 => angular 表单控件也会更新
    45. registerOnChange(fn: any): void {
    46. this.propagateChange = fn;
    47. }
    48. registerOnTouched(): void {}
    49. // 对属性 teacherName 的一些操作事件
    50. onClick($event) {
    51. // ...
    52. }
    53. ngOnInit(): void {
    54. }
    55. }
    56. // 声明校验函数
    57. export function validateTeacherName(c: FormControl) {
    58. let err = {
    59. Error: {
    60. given: c.value,
    61. name: 'error'
    62. }
    63. };
    64. console.log(c.value);
    65. return c.value === '123' ? err : null;
    66. }
    1. <nz-form-item>
    2. <nz-form-label [nzSpan]="4" nzRequired>反馈人</nz-form-label>
    3. <app-teacher-input formControlName="teacherName"></app-teacher-input>
    4. <p *ngIf="validateForm.value.teacherName === '123'">反馈人姓名存在问题,请更换</p>
    5. </nz-form-item>
    1. ngOnInit(): void {
    2. this.validateForm = this.fb.group({
    3. teacherName: ['鲁欣', [validateTeacherName]]
    4. });
    5. }

    参考链接:
    forwardRef 详解
    自定义表单