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

Angular嵌套响应式表单markAllAsTouched()不生效问题求助

Angular嵌套响应式表单提交时标记所有字段为touched失效问题

问题概述

在Angular中实现嵌套响应式表单时,点击提交按钮调用markAllAsTouched()和updateValueAndValidity()后,自定义输入组件内的输入框未被标记为touched,导致必填错误提示无法显示。

尝试通过订阅父表单的statusChanges手动标记内部控件:

if (!this.control.touched && this.parentForm.touched) {
  this.control.markAsTouched();
  this.control.updateValueAndValidity();
}

但该逻辑会导致只要有一个输入框被输入,所有表单控件都会被标记为touched,错误提示过早显示。

需求:点击提交按钮时,一次性将所有嵌套表单元素标记为touched并触发验证。


现有代码结构

根组件(AppComponent)

模板

<form [formGroup]="testForm">
  <app-myinput [parentForm]="testForm" label="Comment" type="text" formControlName="comment"></app-myinput>
  <app-myinput [parentForm]="testForm" label="Name" type="text" formControlName="name"></app-myinput>
  <app-myinput [parentForm]="testForm" label="Email" type="text" formControlName="email"></app-myinput>
  <app-myinput [parentForm]="testForm" label="Password" type="text" formControlName="password"></app-myinput>
  <button style="margin: 1em;" (click)="onSubmit()">Submit</button>
</form>

组件类

import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup } from '@angular/forms';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit {
  title = 'form-test';

  testForm!: FormGroup;

  constructor(private _formBuilder: FormBuilder) { }

  ngOnInit(): void {
    this.testForm = this._formBuilder.group({
      comment: [],
      name: [],
      email: [],
      password: [],
    });
  }

  onSubmit() {
    console.log(this.testForm.valid)
    this.testForm.markAllAsTouched();
    this.testForm.updateValueAndValidity();
  }
}

自定义输入组件(MyinputComponent)

模板

<form [formGroup]="form" style="padding: 1em;">
    <label style="padding-right: 1em">{{label}}</label>
    <input id="value" formControlName="value" type="text" [required]="required">
    <small style="color: rgb(209, 67, 67)" *ngIf="control.touched && control.hasError('required')">
        required
    </small>
</form>

组件类

import { AfterViewInit, Component, forwardRef, Input, OnDestroy, OnInit } from '@angular/core';
import { ControlValueAccessor, FormBuilder, FormControl, FormGroup, NG_VALIDATORS, NG_VALUE_ACCESSOR, Validators } from '@angular/forms';
import { Subscription } from 'rxjs';

@Component({
  selector: 'app-myinput',
  templateUrl: './myinput.component.html',
  styleUrls: ['./myinput.component.scss'],
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => MyinputComponent),
      multi: true
    },
    {
      provide: NG_VALIDATORS,
      useExisting: forwardRef(() => MyinputComponent),
      multi: true
    }
  ]
})
export class MyinputComponent implements ControlValueAccessor, AfterViewInit, OnDestroy {

  form: FormGroup;

  subscriptions: Subscription[] = [];

  @Input()
  label: string = '';

  @Input()
  formControlName = '';

  @Input()
  parentForm!: FormGroup;

  @Input()
  required: boolean = true;

  get control() {
    return this.form.controls['value'];
  }

  get value(): any {
    return this.form.value;
  }

  set value(value: any) {
    this.control.setValue(value)
    this.onChange(value);
    this.onTouched();
  }

  writeValue(value: string): void {

  }

  constructor(private formBuilder: FormBuilder) {
    this.form = this.formBuilder.group({
      value: ['', Validators.required]
    });

    this.subscriptions.push(
      // any time the inner form changes update the parent of any change
      this.form.valueChanges.subscribe(value => {
        this.onChange(value.value);
        this.onTouched();

        this.manipulateValidators();
      })
    );
  }

  ngAfterViewInit(): void {
    this.addStatusChangeSubscription();
    this.addValueChangeSubscription();
  }

  ngOnDestroy(): void {
    this.subscriptions.forEach(s => s.unsubscribe());
  }

  addStatusChangeSubscription() {
    this.subscriptions.push(
      this.parentForm.statusChanges.subscribe(() => {
        if (this.parentForm.disabled && !this.form.disabled) {
          this.form.disable();
        }

        if (this.parentForm.enabled && !this.form.enabled) {
          this.form.enable();
        }

        if (!this.control.touched && this.parentForm.touched) {
          this.control.markAsTouched();
          this.control.updateValueAndValidity();
        }
      })
    );
  }

  addValueChangeSubscription() {
    this.subscriptions.push(
      this.parentForm.valueChanges.subscribe(value => {
        if (this.value.value !== value[this.formControlName]) {
          this.form.patchValue({ value: value[this.formControlName] }, { emitEvent: false })
        }
      })
    );
  }

  validate(_: FormControl) {
    let formInvalid: any = {};
    formInvalid[this.formControlName] = { valid: false };

    return this.form.valid ? null : formInvalid;
  }

  onChange: any = () => {
    /* Left blank intentionally */
  };
  onTouched: any = () => {
    /* Left blank intentionally */
  };

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

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

  manipulateValidators(): void {
    if (this.required && !this.form.controls['value'].hasValidator(Validators.required)) {
      this.form.controls['value'].addValidators(Validators.required);
    }
    if (!this.required && this.form.controls['value'].hasValidator(Validators.required)) {
      this.form.controls['value'].removeValidators(Validators.required);
    }
  }

}

问题原因及解决方案

核心问题

自定义输入组件实现了ControlValueAccessor,但父表单的markAllAsTouched()只会标记父表单自身的FormControl为touched,不会穿透到组件内部的FormGroup控件。而之前通过parentForm.statusChanges监听的逻辑,会在父表单状态变化(比如任意子控件输入导致状态变更)时触发,导致所有内部控件被提前标记。

正确解决方案

方案1:通过ControlValueAccessor同步touched状态

修改自定义组件的registerOnTouched方法,当父控件被标记为touched时,同步标记内部控件:

registerOnTouched(fn: any): void {
  this.onTouched = () => {
    fn();
    // 当父控件触发touched时,同步标记内部控件
    this.control.markAsTouched();
  };
}

根组件的onSubmit中调用markAllAsTouched()时,会触发所有父控件的onTouched回调,进而同步标记内部控件。

方案2:直接在提交时遍历父表单控件,通知子组件标记

  1. 给自定义组件添加公共方法:
markAsTouched() {
  this.control.markAsTouched();
  this.control.updateValueAndValidity();
}
  1. 在根组件中通过ViewChildren获取所有子组件实例,调用该方法:
import { Component, OnInit, ViewChildren, QueryList } from '@angular/core';
import { FormBuilder, FormGroup } from '@angular/forms';
import { MyinputComponent } from './myinput/myinput.component';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit {
  title = 'form-test';

  testForm!: FormGroup;
  @ViewChildren(MyinputComponent) myInputs!: QueryList<MyinputComponent>;

  constructor(private _formBuilder: FormBuilder) { }

  ngOnInit(): void {
    this.testForm = this._formBuilder.group({
      comment: [],
      name: [],
      email: [],
      password: [],
    });
  }

  onSubmit() {
    console.log(this.testForm.valid)
    this.testForm.markAllAsTouched();
    this.testForm.updateValueAndValidity();
    // 通知所有子组件标记内部控件
    this.myInputs.forEach(input => input.markAsTouched());
  }
}

方案3:优化自定义组件的状态监听逻辑

在根组件提交时设置标记,子组件仅在该标记存在时同步状态:

  1. 根组件添加标记并修改提交方法:
isSubmitting = false;

onSubmit() {
  console.log(this.testForm.valid)
  this.isSubmitting = true;
  this.testForm.markAllAsTouched();
  this.testForm.updateValueAndValidity();
  // 验证后重置标记
  setTimeout(() => this.isSubmitting = false, 0);
}
  1. 模板传递标记给子组件:
<app-myinput [parentForm]="testForm" [isSubmitting]="isSubmitting" label="Comment" type="text" formControlName="comment"></app-myinput>
  1. 子组件添加输入属性并修改监听逻辑:
@Input() isSubmitting = false;

addStatusChangeSubscription() {
  this.subscriptions.push(
    this.parentForm.statusChanges.subscribe(() => {
      // 原有禁用/启用逻辑保留
      if (this.parentForm.disabled && !this.form.disabled) {
        this.form.disable();
      }
      if (this.parentForm.enabled && !this.form.enabled) {
        this.form.enable();
      }

      // 仅在提交时同步touched状态
      if (this.isSubmitting && !this.control.touched && this.parentForm.touched) {
        this.control.markAsTouched();
        this.control.updateValueAndValidity();
      }
    })
  );
}

内容的提问来源于stack exchange,提问作者to-cl

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.24 21:24:23