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

Angular自定义表单控件与自定义验证器同步异常问题

Angular自定义表单控件错误同步问题修复

问题现象

实现ControlValueAccessor与Validator接口的自定义表单控件,配合响应式表单及自定义验证器使用时,出现错误信息无法同步的问题:

  • 内部输入控件的errors始终为null
  • 外部表单控件始终显示dataFormat错误(规则为输入长度大于2时有效)

问题根源

  1. 自定义控件验证逻辑错位:当前validate方法直接返回外部控件的errors,但内部表单未应用任何验证规则,也未同步外部验证结果到内部控件状态。
  2. 内外验证触发机制脱节:内部输入值变化时,未通知外部表单重新执行验证;同时外部验证结果也未同步回内部控件,导致内外状态不一致。

修复方案

修改自定义控件代码,实现内外验证规则统一、状态双向同步:

修改后的自定义控件代码

import { Component, forwardRef, OnInit } from '@angular/core';
import { AbstractControl, FormBuilder, FormGroup, ValidationErrors, Validator } from '@angular/forms';
import { ControlValueAccessor, NG_VALIDATORS, NG_VALUE_ACCESSOR } from '@angular/forms';
import { CommonModule } from '@angular/common';
import { ReactiveFormsModule } from '@angular/forms';

@Component({
  selector: 'custom-input',
  standalone: true,
  imports: [ReactiveFormsModule, CommonModule],
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      multi: true,
      useExisting: forwardRef(() => CustomInput),
    },
    {
      provide: NG_VALIDATORS,
      multi: true,
      useExisting: forwardRef(() => CustomInput),
    },
  ],
  template: `
    <article style="border: 1px solid green; padding: .5rem ">
      <form [formGroup]="inputForm">
        <input formControlName="telInput" placeholder="Telephone" (blur)="onFocusOut($event);">
      </form>
    <section class="code">
      inner errors: {{ inputForm.get('telInput')?.errors | json }}
    </section >
</article>
  `,
})
export class CustomInput implements ControlValueAccessor, Validator, OnInit {
  inputForm!: FormGroup;

  formCallbacks = {
    onChange: (anyVal: any) => {},
    onTouched: () => {},
    onValidatorChange: () => {},
  };

  writeValue(obj: any): void {
    // 避免触发不必要的valueChanges循环
    this.inputForm.patchValue({ telInput: obj }, { emitEvent: false });
  }

  registerOnChange(fn: any): void {
    this.formCallbacks.onChange = fn;
  }

  registerOnTouched(fn: any): void {
    this.formCallbacks.onTouched = fn;
  }

  setDisabledState?(isDisabled: boolean): void {
    isDisabled ? this.inputForm.disable({ emitEvent: false }) : this.inputForm.enable({ emitEvent: false });
  }

  // 基于内部控件状态返回验证结果
  validate(control: AbstractControl): ValidationErrors | null {
    const innerControl = this.inputForm.get('telInput');
    return innerControl?.errors || null;
  }

  registerOnValidatorChange?(fn: () => void): void {
    this.formCallbacks.onValidatorChange = fn;
  }

  constructor(private fb: FormBuilder) {}

  ngOnInit() {
    // 内部表单添加与外部一致的验证规则
    this.inputForm = this.fb.group({
      telInput: ['', [this.someValidator]],
    });

    // 内部值变化时,同步到外部并触发验证更新
    this.inputForm.valueChanges.subscribe((val) => {
      this.formCallbacks.onChange(val.telInput);
      // 通知外部重新验证
      this.formCallbacks.onValidatorChange();
    });
  }

  onFocusOut(event: any) {
    this.formCallbacks.onTouched();
  }

  // 复用与外部一致的验证逻辑
  private someValidator(ac: AbstractControl): ValidationErrors | null {
    if (ac.value === null || ac.value === '') {
      return null;
    }
    const noErrorsOk = ac.value.length > 2;
    return noErrorsOk ? null : { dataFormat: true };
  }
}

关键修改说明

  1. 统一验证规则:在内部表单控件中添加与外部完全一致的验证方法someValidator,确保内外验证逻辑统一。
  2. 修正validate方法:不再返回外部控件的错误,而是基于内部表单控件的错误状态返回结果,保证验证来源唯一。
  3. 同步触发验证:在内部值变化时,调用onValidatorChange()通知外部表单重新执行验证,确保外部状态随内部输入实时更新。
  4. 优化状态设置:在writeValue和setDisabledState中使用emitEvent: false,避免触发不必要的valueChanges循环。

消费者组件代码(无需修改)

import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, ValidationErrors } from '@angular/forms';
import { CustomInput } from './custom-input.component';
import { ReactiveFormsModule } from '@angular/forms';
import { CommonModule } from '@angular/common';

function someValidator(ac: AbstractControl): ValidationErrors | null {
  if (ac.value === null || ac.value === '') {
    return null;
  }
  const inputVal = ac.value;
  const noErrorsOk = inputVal.length > 2;
  const err = noErrorsOk ? null : { dataFormat: true };
  console.log('[outer] validator err', err);
  return err;
}

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [CustomInput, ReactiveFormsModule, CommonModule],
  template: `
    <article>
      <form [formGroup]="mainForm">
        <custom-input formControlName="joesNumber"></custom-input>
      </form>
      
      <section class="code">
        outer error: {{ mainForm.get('joesNumber')?.errors | json }}
      </section>

      <section>
        <p>Notes: length > 2 is valid. inner error is always null and outer error is alwasy dataFormat error. </p>
      </section>
    </article>
  `,
})
export class App implements OnInit {
  mainForm!: FormGroup;

  constructor(private fb: FormBuilder) {}

  ngOnInit() {
    this.mainForm = this.fb.group({
      joesNumber: ['', [someValidator]],
    });
    this.mainForm.statusChanges.subscribe((val) => {
      console.log('[outer] statusChange', val);
    });
  }
}

修复后,内部控件的错误状态会与外部表单同步,输入长度大于2时内外错误均为null,不符合规则时均显示dataFormat错误。

内容的提问来源于stack exchange,提问作者vito

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.05 13:50:05