Angular中实现ControlValueAccessor的自定义输入标记为touched的问题解决
需求
点击提交按钮但未填写所有输入框时,需将所有输入标记为touched状态;由于表单配置了必填校验器,空输入框应显示红色边框。
问题
调用表单的markAllAsTouched()方法标记所有输入为touched状态后,自定义输入框未发生变化,未添加对应的样式类,仅原生输入框显示红色边框。
输入组件代码
TypeScript 代码
import {AfterContentChecked, Component, Input, OnInit, Optional, Self} from '@angular/core'; import {ControlValueAccessor, NgControl, UntypedFormControl, UntypedFormGroup} from '@angular/forms'; @Component({ selector: 'app-input', templateUrl: './input.component.html', styleUrls: ['./input.component.css'], providers: [], }) export class InputComponent implements ControlValueAccessor, OnInit { group = new UntypedFormGroup({ input: new UntypedFormControl(''), }); touched: boolean = false; @Input() label: string = ''; private ngControl: NgControl; constructor(@Optional() @Self() ngControl: NgControl) { this.ngControl = ngControl; if (this.ngControl) { this.ngControl.valueAccessor = this; } } _value: string = ''; get value(): string { return this._value; } @Input() set value(value: string) { this._value = value; this.input.setValue(value, {emitEvent: false}); } _disabled: boolean = false; get disabled(): boolean { return this._disabled; } @Input() set disabled(disabled: boolean) { this._disabled = disabled; if (disabled) { this.input.disable(); } else { this.input.enable(); } } get input() { return this.group.get('input') as UntypedFormControl; } ngOnInit(): void { if (this.ngControl !== null) { this.ngControl.control?.statusChanges.subscribe((status) => { if (status === 'INVALID') { this.input.markAsDirty(); this.input.markAsTouched(); this.input.setErrors({incorrect: true}); } }); } else { console.warn('no formcontrol'); } } onTouched = () => { }; writeValue(value: string): void { this.input.setValue(value, {emitEvent: false}); } registerOnChange(fn: any): void { this.input.valueChanges.subscribe((value) => { this.markAsTouched(); fn(value); }); } registerOnTouched(fn: any): void { this.onTouched = fn; } markAsTouched() { if (!this.touched) { this.onTouched(); this.touched = true; } } setDisabledState?(isDisabled: boolean): void { if (isDisabled) { this.input.disable(); } else { this.input.enable(); } } }
HTML 模板
<span [formGroup]="group"> <label [innerHTML]="label" ></label> <input class="w-full" formControlName="input" ngDefaultControl type="text"> </span>
根组件代码
TypeScript 代码
import { Component } from '@angular/core'; import {UntypedFormControl, UntypedFormGroup, Validators} from "@angular/forms"; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'] }) export class AppComponent { title = 'test-input'; myForm = new UntypedFormGroup({ inputBasic: new UntypedFormControl('',[Validators.required]), inputCustom: new UntypedFormControl('',[Validators.required]), }) submit(){ if(this.myForm.valid){ console.log('valid') }else{ this.myForm.markAllAsTouched(); // 若添加下方代码,自定义输入框会更新并正常显示样式 // this.myForm.get('inputCustom')?.setErrors({errorMessage:'Error'}) } } }
HTML 模板
<form [formGroup]="myForm" (submit)="submit()"> <div> 原生输入框 <input formControlName="inputBasic"/> </div> <div> <app-input label="自定义输入框" formControlName="inputCustom"></app-input> </div> <button type="submit">提交</button> </form>
共享 CSS 代码
.ng-invalid.ng-touched{ border-color:red; }
问题原因与解决方案
原因
自定义输入组件内部维护了独立的UntypedFormGroup和UntypedFormControl,外部表单的markAllAsTouched()仅标记外部的inputCustom控件为touched,但组件内部的input控件未同步该状态。同时,组件ngOnInit仅在状态变为INVALID时标记内部控件为touched,而markAllAsTouched()不会触发状态变化(控件原本已是INVALID),导致内部控件状态未更新。
解决方法
方法一:同步外部控件的touched状态到内部控件
修改InputComponent的ngOnInit方法,添加对外部控件touched状态的监听:
ngOnInit(): void { if (this.ngControl !== null) { // 监听外部控件的touched状态变化 const control = this.ngControl.control; if (control) { control.statusChanges.subscribe(() => { if (control.touched) { this.input.markAsTouched(); this.touched = true; } }); } this.ngControl.control?.statusChanges.subscribe((status) => { if (status === 'INVALID') { this.input.markAsDirty(); this.input.setErrors({incorrect: true}); } }); } else { console.warn('no formcontrol'); } }
方法二:移除内部表单组,直接关联外部控件
简化组件逻辑,直接使用外部NgControl:
import { Component, Input, OnInit, Optional, Self } from '@angular/core'; import { ControlValueAccessor, NgControl } from '@angular/forms'; @Component({ selector: 'app-input', templateUrl: './input.component.html', styleUrls: ['./input.component.css'], }) export class InputComponent implements ControlValueAccessor, OnInit { @Input() label: string = ''; touched: boolean = false; constructor(@Optional() @Self() public ngControl: NgControl) { if (this.ngControl) { this.ngControl.valueAccessor = this; } } ngOnInit(): void {} onTouched = () => {}; onChange = (value: string) => {}; writeValue(value: string): void {} registerOnChange(fn: any): void { this.onChange = fn; } registerOnTouched(fn: any): void { this.onTouched = fn; } markAsTouched() { if (!this.touched) { this.onTouched(); this.touched = true; } } setDisabledState?(isDisabled: boolean): void { if (this.ngControl.control) { isDisabled ? this.ngControl.control.disable() : this.ngControl.control.enable(); } } }
修改模板:
<span> <label [innerHTML]="label"></label> <input class="w-full" [formControl]="ngControl.control" type="text" (blur)="markAsTouched()" > </span>
此方式下,外部markAllAsTouched()会直接作用到控件,样式类将正常添加。
内容的提问来源于stack exchange,提问作者Gregory Boutte
相关产品推荐
相关产品推荐

