Angular自定义表单控件如何在组件内部判断invalid与touched状态?
解决自定义Control Value Accessor控件获取NgControl状态的问题
问题根源
直接注入NgControl报错,是因为当自定义控件未被Angular表单指令(如formControl/formControlName)绑定时,NgControl实例不存在,导致注入失败。此外,即使注入成功,若未做空值判断直接读取statusChanges,也会触发TypeError。
解决方案步骤
- 使用
@Optional装饰器注入NgControl,避免无提供者时抛出错误 - 在
ngOnInit中安全获取并订阅状态,加入空值判断 - 处理订阅销毁,防止内存泄漏(需实现
OnDestroy接口)
完整修正后的组件代码
import { ControlValueAccessor, NG_VALUE_ACCESSOR, NgControl, OnInit, OnDestroy, Optional, ChangeDetectorRef } from '@angular/forms'; import { Component, Input, Output, EventEmitter, forwardRef } from '@angular/core'; import { Subscription } from 'rxjs'; @Component({ selector: 'custom-textbox', templateUrl: './custom-textbox.component.html', styleUrls: ['./custom-textbox.component.scss'], providers: [ { provide: NG_VALUE_ACCESSOR, multi: true, useExisting: forwardRef(() => CustomTextboxComponent), }, ], changeDetection: ChangeDetectionStrategy.OnPush, }) export class CustomTextboxComponent implements ControlValueAccessor, OnInit, OnDestroy { @Input('Label') label: string = ''; @Input('Type') type: string = 'text'; @Input('PlaceHolder') placeHolder: string = ''; @Input('ReadOnly') readOnly: boolean = false; @Input('AutoFocus') autoFocus: boolean = false; @Input('MaxLength') maxLength: number = 524288; @Output('Input') input: EventEmitter<any> = new EventEmitter(); _value: string = ''; onChange = (value: any) => {}; onTouched = () => {}; touched: boolean = false; disabled: boolean = false; private statusSubscription?: Subscription; private ngControl?: NgControl; constructor( @Optional() private injectedNgControl: NgControl, private readonly changeDetectorRef: ChangeDetectorRef ) {} ngOnInit() { this.ngControl = this.injectedNgControl; if (this.ngControl?.control) { this.statusSubscription = this.ngControl.control.statusChanges.subscribe(() => { const isInvalid = this.ngControl?.control?.invalid; const isTouched = this.ngControl?.control?.touched; if (isInvalid && isTouched) { // 此处执行显示提示框、错误原因的逻辑 console.log('控件无效且已触碰', this.ngControl.control.errors); } this.changeDetectorRef.markForCheck(); }); } } ngOnDestroy() { // 销毁订阅,避免内存泄漏 this.statusSubscription?.unsubscribe(); } set value(value: any) { this._value = value; this.onChange(value); this.changeDetectorRef.detectChanges(); } get value(): any { return this._value; } // 补全ControlValueAccessor核心方法 writeValue(value: any): void { this._value = value || ''; this.changeDetectorRef.markForCheck(); } registerOnChange(fn: any): void { this.onChange = fn; } registerOnTouched(fn: any): void { this.onTouched = fn; } setDisabledState(isDisabled: boolean): void { this.disabled = isDisabled; this.changeDetectorRef.markForCheck(); } // 自定义输入事件处理 onInput(event: Event) { const target = event.target as HTMLInputElement; this.value = target.value; this.input.emit(target.value); } onBlur() { this.touched = true; this.onTouched(); this.changeDetectorRef.markForCheck(); } }
关键修正点说明
@Optional()装饰器:告知Angular当NgControl不存在时注入null,而非抛出错误- 空值安全访问:使用
?.操作符判断ngControl及control实例是否存在,避免TypeError - 订阅销毁:在组件销毁时取消
statusChanges订阅,防止内存泄漏 - 补全ControlValueAccessor方法:之前代码缺失核心方法,会导致表单控件无法正常同步状态
模板示例(custom-textbox.component.html)
<div class="custom-textbox"> <label *ngIf="label">{{ label }}</label> <input [type]="type" [value]="value" [placeholder]="placeHolder" [readonly]="readOnly" [maxlength]="maxLength" [disabled]="disabled" (input)="onInput($event)" (blur)="onBlur()" > <!-- 错误提示区域 --> <div *ngIf="ngControl?.control?.invalid && ngControl?.control?.touched" class="error-message"> <span *ngIf="ngControl.control?.errors?.['required']">此字段为必填项</span> <span *ngIf="ngControl.control?.errors?.['minlength']">长度不能少于{{ ngControl.control.errors['minlength'].requiredLength }}位</span> <!-- 可扩展其他错误类型提示 --> </div> </div>
内容的提问来源于stack exchange,提问作者Hüsnücan Demirci
相关产品推荐
相关产品推荐

