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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.03 11:31:07