You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Angular ControlValueAccessor嵌套时registerOnChange未调用但FormControl值仍更新

问题描述

我正在开发自定义表单模板,目标是从XML文件动态构建表单,目前为了测试,直接在App组件中创建输入元素,这些输入项有时会嵌套在不同的分组中。
为此我实现了Form-Group组件和通用Form-Element组件,此前实现ControlValueAccessor后一切运行正常。
但现在为了支持不同类型的自定义FormControl,我尝试将Form-Element组件改造为通用包装组件,由它根据XML提供的Form-Element模型的类型(文本、复选框、开关等)调用对应的子组件。
因此ControlValueAccessor现在移至子组件TextInputComponent中实现。
但完成子组件改造后,架构未按预期运行:当我在属于TextInputComponent的输入框中输入值时,总会抛出ERROR TypeError: this.onChange is not a function错误。我定位到问题是registerOnChange方法未被调用,即Angular未按要求识别该组件的ControlValueAccessor实现。但奇怪的是,Angular FormGroup中的字段值仍能正常更新。
请问我该如何消除这个错误?

相关代码

Form-Group组件

HTML

<div class="row border">
    <div class="col">
        <p>{{formGroupModel.id}}</p>
        <form [formGroup]="curFormGroup">
            <ng-container *ngFor="let item of formGroupModel.Elements">
                <hyc-formgroup *ngIf="utils.getInstanceType(item) === 'FormGroupModel'"
                                [parentFormGroup]="curFormGroup"
                                [formGroupModel]="item">
                </hyc-formgroup>
                <hyc-formelement *ngIf="utils.getInstanceType(item) === 'FormElementModel'"
                                [parentFormGroup]="curFormGroup"
                                [formElementModel]="item">
                </hyc-formelement>
            </ng-container>
        </form>
    </div>
</div>

TS

import { Component, forwardRef, Input, OnInit } from "@angular/core";
import { ControlValueAccessor, FormArray, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from "@angular/forms";
import { Utils } from "src/app/UTILS/INSTANCE_TYPE";
import { FormElementModel } from "../form-element/form-element.model";
import { FormGroupModel } from "./form-group.model";

@Component({
  selector: 'hyc-formgroup',
  templateUrl: './form-group.component.html',
  styleUrls: ['./form-group.component.css'],
})
export class FormGroupComponent implements OnInit {
  @Input()
  public formGroupModel : any;
  @Input() public parentFormGroup: FormGroup;
  constructor (
    public formBuilder : FormBuilder,
    public utils : Utils
  ) {}
  ngOnInit(): void {
      this.initFormGroup();
  }
  initFormGroup() {
    this.parentFormGroup.addControl(
      this.formGroupModel.id, this.formBuilder.group({})
    );
  }
  get curFormGroup() {
    return this.parentFormGroup.get(this.formGroupModel.id) as FormGroup;
  }
}

Form-Element包装组件

HTML

<ng-container *ngIf="formElementModel.type == 'text'">
    <hyc-text-input [parentFormGroup]="parentFormGroup"
                    [formElementModel]="formElementModel">
    </hyc-text-input>
</ng-container>

TS

import { Component, Input } from "@angular/core";
import { FormGroup } from "@angular/forms";

@Component({
  selector: 'hyc-formelement',
  templateUrl: './form-element.component.html',
  styleUrls: ['./form-element.component.css'],
})
export class FormElementComponent {
  @Input() parentFormGroup : FormGroup;
  @Input() formElementModel : any;
  public value : any;
  constructor() {}
}

TextInput实际表单控件组件

HTML

<input  type="text"
        class="form-control my-1"
        placeholder="{{formElementModel.id}}"
        [value]="value"
        (input)="_onChange( $event )"
        [formControl]="formControlObj"/>

TS

import { Component, Input, OnInit } from '@angular/core';
import { ControlValueAccessor, FormBuilder, NG_VALUE_ACCESSOR } from '@angular/forms';
import { Utils } from "src/app/UTILS/INSTANCE_TYPE";

@Component({
  selector: 'hyc-text-input',
  templateUrl: './text-input.component.html',
  styleUrls: ['./text-input.component.css'],
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: TextInputComponent,
      multi: true
    }
  ]
})
export class TextInputComponent implements OnInit, ControlValueAccessor {
  @Input() formElementModel;
  @Input() parentFormGroup;
  formControlObj;
  public value : any;
  public onChange: (value: any) => void;
  constructor (
     public formBuilder : FormBuilder,
    public utils : Utils
   ) { }
  ngOnInit(): void {
    this.formElementModel.defaultValue ? this.value = this.formElementModel.defaultValue : this.value = null;
    this.formControlObj = this.formBuilder.control( this.value );
    this.parentFormGroup.addControl( this.formElementModel.id, this.formControlObj );
  }
  _onChange( event: Event) {
    const value = (<HTMLInputElement>event.target).value;
    this.onChange(value);
    this.value = value;
  }
  writeValue(value: any): void {
    if(!value || value == null) return
    this.value = value;
  }
  registerOnChange(fn: (value : any) => void): void {
    this.onChange = fn;
  }
  registerOnTouched(fn: any): void {
  }
  setDisabledState?(isDisabled: boolean): void {
  }
}

FormPage外层组件

HTML

<div class="row">
    <div class="col">
        <div class="text-center">
            <h1>{{formNavi.Titel}}</h1>
            <h2> {{formNavi.CurPageObject.Name}}</h2>
            <p>带*标记的字段为必填项</p>
        </div>
        <form [formGroup]="curPageFormGroup">
            <ng-container *ngFor="let item of formNavi.CurPageObject.Elements">
                    <hyc-formgroup *ngIf="utils.getInstanceType(item) == 'FormGroupModel'"
                                    [formGroupName]="item.id"
                                    [parentFormGroup]="curPageFormGroup"
                                    [formGroupModel]="item">
                    </hyc-formgroup>
                    <hyc-formelement *ngIf="utils.getInstanceType(item) == 'FormElementModel'"
                                    [parentFormGroup]="curPageFormGroup"
                                    [formElementModel]="item">
                    </hyc-formelement>
            </ng-container>
        </form>
    </div>
</div>
<pre> {{wrapperFormGroup.value | json}} </pre>
<button (click)="onClickTest()">TEST</button>
<button (click)="formNavi.nextPage()">NEXT PAGE</button>

TS

import { Component, Input, OnInit } from "@angular/core";
import { FormBuilder, FormGroup, Validators } from "@angular/forms";
import { Subscription } from "rxjs";
import { NavigationService } from "../services/navigation.service";
import { Utils } from "../UTILS/INSTANCE_TYPE";

@Component({
    selector: "hyc-formpage",
    templateUrl: "./form-page.component.html",
    styleUrls: ["./form-page.component.css"]
})
export class FormPageComponent implements OnInit {
    @Input() wrapperFormGroup : FormGroup;
    curPageValue : Subscription;
    constructor (
        public formNavi : NavigationService,
        public formBuilder : FormBuilder,
        public utils : Utils
    ) { }
    ngOnInit(): void {
    }
    get curPageFormGroup() {
        return this.wrapperFormGroup.get(this.formNavi.CurPageObject.ID) as FormGroup;
    }
}

解决方案

错误的根本原因是:你实现了ControlValueAccessor并注册了NG_VALUE_ACCESSOR,但在使用hyc-text-input组件时没有绑定formControl或formControlName指令,Angular不会主动调用registerOnChange方法,导致this.onChange一直是undefined,输入时调用就会报错。而FormGroup值能正常更新,是因为你手动创建了formControlObj并添加到了父FormGroup中,输入框直接绑定了这个内部的FormControl,值更新会直接同步到父FormGroup。

方案1:移除不必要的ControlValueAccessor实现

你现在的实现里手动管理FormControl的创建、挂载,完全用不到ControlValueAccessor,直接删除相关代码即可:

  1. 删掉TextInputComponent的@Component配置里的providers数组中NG_VALUE_ACCESSOR的注册项
  2. 删除ControlValueAccessor接口的实现,包括writeValue、registerOnChange、registerOnTouched、setDisabledState方法,以及onChange属性声明
  3. 修改_onChange方法,删掉this.onChange(value)这一行即可

方案2:保留ControlValueAccessor,调整用法

如果要保留CVA实现来符合Angular表单控件的标准规范,可以做如下调整:

  1. 删掉TextInputComponent中手动创建formControlObj、给父FormGroup添加控件的相关代码,CVA模式下不需要手动管理父FormGroup的控件挂载
  2. 在FormElement组件上也实现ControlValueAccessor,将值变更透传给对应的子控件,或者在调用子组件时绑定formControlName
  3. 确保使用hyc-text-input的上层位置正确传入formControl或formControlName,触发Angular调用CVA的相关方法

内容的提问来源于stack exchange,提问作者artuk

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.28 15:39:03