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

Angular 9:使用内置Ng指令验证子输入组件

解决Angular自定义文本输入组件的表单验证问题

我完全懂你现在的困扰:封装了通用文本输入组件后,不仅内置的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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.29 15:34:09