Angular响应式表单使用patchValue更新禁用状态失效及报错问题
问题修复方案
问题根因
ExpressionChangedAfterItHasBeenCheckedError报错:切换toggle按钮时,你在同一个变更检测周期内同步修改了表单校验规则,直接触发了表单valid状态变更,导致提交按钮的disabled属性在Angular开发环境的两次变更检测校验中值不一致,因此抛出该错误。- 切回Email模式按钮仍禁用:Angular响应式表单调用
setValidators/clearValidators修改校验规则后,不会自动重新计算控件和父表单的校验状态,必须手动触发状态更新,你未执行该操作导致表单状态没有同步刷新。
修复步骤
1. 修改app.component.ts的初始化逻辑
表单初始化完成后主动触发一次校验规则设置,给默认选中的Email模式加上对应校验:
import { Component, OnInit, VERSION } from '@angular/core'; import {FormBuilder, FormGroup, Validators} from '@angular/forms'; @Component({ selector: 'my-app', templateUrl: './app.component.html', styleUrls: [ './app.component.css' ] }) export class AppComponent implements OnInit { name = 'Angular ' + VERSION.major; DELIVERY_MODES = { sms: 'sms', email: 'email' }; deliveryForm: FormGroup; constructor(private formBuilder: FormBuilder) {} ngOnInit(): void { const dummyData = new PlatformEnrollmentDelivery(); dummyData.hour = 10; dummyData.minute = 30; dummyData.modeTarget = "example@domain.com" this.deliveryForm = this.formBuilder.group({ deliveryConfiguration: this.initDeliveryModeFormGroup(dummyData), setDefaults: this.formBuilder.group({ setDefault: false }) }); // 新增:初始化默认校验规则 this.setDeliveryMethodType(this.DELIVERY_MODES.email); } // 其余原有初始化方法保持不变 initDeliveryModeFormGroup(delivery: PlatformEnrollmentDelivery): any { const defaultMode = delivery.mode ? delivery.mode.toLowerCase() : this.DELIVERY_MODES.email; return this.formBuilder.group({ mode: [defaultMode], email: this.formBuilder.group(this.initEmailDeliveryMode(delivery)), sms: this.formBuilder.group(this.initSmsDeliveryMode(delivery)), deliveryTime: this.formBuilder.group(this.initDeliveryTime(delivery)) }); } initEmailDeliveryMode(delivery: PlatformEnrollmentDelivery): object { const emailControlValue = delivery.mode.toLowerCase() === this.DELIVERY_MODES.email ? delivery.modeTarget : ''; return { emailControl: [emailControlValue] }; } initSmsDeliveryMode(delivery: PlatformEnrollmentDelivery): object { const smsControlValue = delivery.mode.toLowerCase() === this.DELIVERY_MODES.sms ? delivery.modeTarget : ''; return { smsControl: [smsControlValue] }; } initDeliveryTime(delivery: PlatformEnrollmentDelivery): object { return { timeControl: ["10:00"] }; }
2. 重写setDeliveryMethodType方法
把校验规则修改逻辑放到微任务队列避开当前变更检测周期,同时手动触发控件校验状态更新:
setDeliveryMethodType(type: string): void { this.deliveryForm.controls.deliveryConfiguration.get('mode').patchValue(type); // 放到微任务中执行,规避同周期状态变更报错 Promise.resolve().then(() => { const emailControl = this.deliveryForm.controls.deliveryConfiguration.get('email').get('emailControl'); const smsControl = this.deliveryForm.controls.deliveryConfiguration.get('sms').get('smsControl'); switch (type) { case this.DELIVERY_MODES.email: emailControl.setValidators([Validators.required, Validators.email]); smsControl.clearValidators(); break; case this.DELIVERY_MODES.sms: smsControl.setValidators([Validators.required]); emailControl.clearValidators(); break; } // 手动触发控件校验状态更新,同步到父表单 emailControl.updateValueAndValidity(); smsControl.updateValueAndValidity(); }) } } export class PlatformEnrollmentDelivery { id: number; mode: string; hour: number; updated: Date; minute: number modeTarget: string; constructor( ) { this.mode = "email"; } }
可选替代方案
如果你不想用微任务的写法,也可以通过注入ChangeDetectorRef手动触发变更检测解决报错,两种方案效果一致:
- 构造器注入
ChangeDetectorRef:
constructor(private formBuilder: FormBuilder, private cdr: ChangeDetectorRef) {}
- 在
setDeliveryMethodType方法末尾调用this.cdr.detectChanges()即可。
app.component.html代码无需任何修改,修改完成后两个问题都可以解决。
内容的提问来源于stack exchange,提问作者Chris Rockwell
相关产品推荐
相关产品推荐

