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,直接删除相关代码即可:
- 删掉
TextInputComponent的@Component配置里的providers数组中NG_VALUE_ACCESSOR的注册项 - 删除
ControlValueAccessor接口的实现,包括writeValue、registerOnChange、registerOnTouched、setDisabledState方法,以及onChange属性声明 - 修改
_onChange方法,删掉this.onChange(value)这一行即可
方案2:保留ControlValueAccessor,调整用法
如果要保留CVA实现来符合Angular表单控件的标准规范,可以做如下调整:
- 删掉
TextInputComponent中手动创建formControlObj、给父FormGroup添加控件的相关代码,CVA模式下不需要手动管理父FormGroup的控件挂载 - 在
FormElement组件上也实现ControlValueAccessor,将值变更透传给对应的子控件,或者在调用子组件时绑定formControlName - 确保使用
hyc-text-input的上层位置正确传入formControl或formControlName,触发Angular调用CVA的相关方法
内容的提问来源于stack exchange,提问作者artuk
相关产品推荐
相关产品推荐

