Angular 9:使用内置Ng指令验证子输入组件
我完全懂你现在的困扰:封装了通用文本输入组件后,不仅内置的required、minLength这些验证规则没法触发错误提示,连Angular自带的pristine、dirty、touched状态也没法在父组件里正常使用,还要给不同组件实例配置差异化的验证规则(比如姓名不能含数字、密码必须有数字)对吧?
核心问题出在你的自定义组件没和Angular表单系统打通——Angular要求组件实现ControlValueAccessor接口,才能让表单指令(比如ngModel)的状态、验证逻辑和组件内部的input元素绑定。下面一步步给你解决:
第一步:让组件实现ControlValueAccessor接口
修改TextControlComponent,实现这个接口,让Angular把它识别为标准表单控件,同步值和状态:
import { Component, Input, ViewChild, ElementRef, forwardRef } from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; @Component({ selector: 'app-text-control', templateUrl: './text-control.component.html', styleUrls: ['./text-control.component.scss'], providers: [ { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => TextControlComponent), multi: true } ] }) export class TextControlComponent implements ControlValueAccessor { @Input() className: string; @Input() disabled = false; @Input() id: string; @Input() minLength: number; @Input() maxLength: number; @Input() name: string; @Input() pattern: string; @Input() placeholder: string; @Input() suffix: string; @Input() size = 25; @Input() type = 'text'; @Input() public required = false; @ViewChild('textControlInput') textControlInput: ElementRef<HTMLInputElement>; // 表单状态跟踪 touched = false; onChange: (value: string) => void = () => {}; onTouched: () => void = () => {}; constructor() {} // 从表单模型同步值到组件 writeValue(value: string): void { this.textControlInput.nativeElement.value = value || ''; } // 注册值变化的回调函数 registerOnChange(fn: (value: string) => void): void { this.onChange = fn; } // 注册触摸状态变化的回调函数 registerOnTouched(fn: () => void): void { this.onTouched = fn; } // 设置组件禁用状态 setDisabledState(isDisabled: boolean): void { this.disabled = isDisabled; this.textControlInput.nativeElement.disabled = isDisabled; } // 处理输入事件,同步值到表单模型 onInputChange(): void { const value = this.textControlInput.nativeElement.value; this.onChange(value); } // 处理失焦事件,标记组件为已触摸 onBlur(): void { if (!this.touched) { this.touched = true; this.onTouched(); } } getCSSClasses() { return 'TextControl' + (this.className ? ' ' + this.className : '') + (this.disabled ? ' disabled' : '') ; } }
然后修改组件模板,去掉原有的[value]绑定(由writeValue处理),新增blur事件绑定:
<input #textControlInput [class]="getCSSClasses()" [id]="id" [name]="name" [type]="type" [minLength]="minLength" [maxLength]="maxLength" [size]="size" [placeholder]="placeholder" [required]="required" [disabled]="disabled" (input)="onInputChange()" (blur)="onBlur()" />
第二步:让内置验证规则生效
现在组件和表单系统连通了,但还要让Angular能收集组件的验证状态。给组件添加NG_VALIDATORS提供器,并实现validate方法:
先更新组件的providers数组:
providers: [ { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => TextControlComponent), multi: true }, { provide: NG_VALIDATORS, useExisting: forwardRef(() => TextControlComponent), multi: true } ]
然后在组件类中添加验证逻辑:
import { AbstractControl, ValidationErrors } from '@angular/forms'; // ...其他代码 validate(control: AbstractControl): ValidationErrors | null { const input = this.textControlInput.nativeElement; const errors: ValidationErrors = {}; // 校验required规则 if (this.required && !input.value.trim()) { errors['required'] = true; } // 校验minLength规则 if (this.minLength && input.value.length < this.minLength) { errors['minlength'] = { requiredLength: this.minLength, actualLength: input.value.length }; } // 校验maxLength规则 if (this.maxLength && input.value.length > this.maxLength) { errors['maxlength'] = { requiredLength: this.maxLength, actualLength: input.value.length }; } // 校验pattern规则 if (this.pattern && !new RegExp(this.pattern).test(input.value)) { errors['pattern'] = { requiredPattern: this.pattern, actualValue: input.value }; } return Object.keys(errors).length ? errors : null; }
第三步:给不同实例配置自定义验证规则
针对姓名不能含数字、密码必须有数字这类差异化需求,有两种实现方式:
方式1:父组件中直接绑定自定义验证器
在parent.component.ts中定义验证函数:
import { FormControl, ValidationErrors } from '@angular/forms'; // 姓名不能包含数字的验证器 nameNoNumbersValidator(control: FormControl): ValidationErrors | null { const hasNumber = /\d/.test(control.value); return hasNumber ? { nameHasNumber: '姓名不能包含数字' } : null; } // 密码必须包含数字的验证器 passwordHasNumberValidator(control: FormControl): ValidationErrors | null { const hasNumber = /\d/.test(control.value); return hasNumber ? null : { passwordNoNumber: '密码必须包含数字' }; }
然后在模板中使用:
<!-- 姓名输入框 --> <app-text-control id="{{idPrefix}}firstName" name="{{idPrefix}}firstName" type="text" [readOnly]="readOnly" className="g-mb-1" [size]="15" [(ngModel)]="loginDetails.firstName" placeholder="First Name" required="true" minLength="3" maxLength="100" #firstName="ngModel" [ngModelOptions]="{ validators: [nameNoNumbersValidator] }" > </app-text-control> <!-- 显示错误提示 --> <div *ngIf="firstName.invalid && (firstName.dirty || firstName.touched)" class="error-text"> <div *ngIf="firstName.errors?.required">姓名不能为空</div> <div *ngIf="firstName.errors?.minlength">姓名至少需要{{firstName.errors.minlength.requiredLength}}个字符</div> <div *ngIf="firstName.errors?.nameHasNumber">{{firstName.errors.nameHasNumber}}</div> </div> <!-- 密码输入框 --> <app-text-control id="{{idPrefix}}password" name="{{idPrefix}}password" type="password" [readOnly]="readOnly" className="g-mb-1" [size]="15" [(ngModel)]="loginDetails.password" placeholder="Password" required="true" minLength="6" #password="ngModel" [ngModelOptions]="{ validators: [passwordHasNumberValidator] }" > </app-text-control> <div *ngIf="password.invalid && (password.dirty || password.touched)" class="error-text"> <div *ngIf="password.errors?.required">密码不能为空</div> <div *ngIf="password.errors?.minlength">密码至少需要{{password.errors.minlength.requiredLength}}个字符</div> <div *ngIf="password.errors?.passwordNoNumber">{{password.errors.passwordNoNumber}}</div> </div>
方式2:组件接收自定义验证器数组
在TextControlComponent中添加输入属性:
import { ValidatorFn } from '@angular/forms'; // ...其他代码 @Input() customValidators: ValidatorFn[] = [];
然后在validate方法中合并自定义验证逻辑:
validate(control: AbstractControl): ValidationErrors | null { // ...之前的内置验证逻辑 // 应用自定义验证器 this.customValidators.forEach(validator => { const error = validator(control); if (error) { Object.assign(errors, error); } }); return Object.keys(errors).length ? errors : null; }
父组件中传递验证器:
<app-text-control <!-- 其他属性 --> [customValidators]="[nameNoNumbersValidator]" > </app-text-control>
第四步:在父组件中使用表单状态
现在你可以通过模板引用变量(比如#firstName=ngModel)访问组件的所有表单状态:
firstName.pristine:输入框未被修改firstName.dirty:输入框已被修改firstName.touched:输入框已被触摸firstName.invalid:输入内容不符合验证规则firstName.errors:包含所有验证错误的对象
根据这些状态就能精准控制错误提示的显示时机了。
内容的提问来源于stack exchange,提问作者OHello

