动态嵌套反应形式:ExpressionChangedAfterItHasBeenCheckedError

Posted

技术标签:

【中文标题】动态嵌套反应形式:ExpressionChangedAfterItHasBeenCheckedError【英文标题】:Dynamic nested reactive form: ExpressionChangedAfterItHasBeenCheckedError 【发布时间】:2018-01-21 11:52:39 【问题描述】:

我的响应式表单是三个组件级别的深度。父组件创建一个没有任何字段的新表单并将其传递给子组件。

起初外部形式是有效的。稍后,子组件会添加带有验证器(失败)的新表单元素,从而使外部表单无效。

我在控制台中收到 ExpressionChangedAfterItHasBeenCheckedError 错误。我想修正那个错误。

不知何故,这只发生在我添加第三层嵌套时。同样的方法似乎适用于两层嵌套。

Plunker: https://plnkr.co/edit/GymI5CqSACFEvhhz55l1?p=preview

父组件

@Component(
  selector: 'app-root',
  template: `
    myForm.valid: <b>myForm.valid</b>
    <form>
      <app-subform [myForm]="myForm"></app-subform>
    </form>
  `
)
export class AppComponent implements OnInit 
  ...

  ngOnInit() 
    this.myForm = this.formBuilder.group();
  

子组件

@Component(
  selector: 'app-subform',
  template: `
    <app-address-form *ngFor="let addressData of addressesData;"
      [addressesForm]="addressesForm">
    </app-address-form>
  `
)
export class SubformComponent implements OnInit 
  ...

  addressesData = [...];

  constructor()  

  ngOnInit() 
    this.addressesForm = new FormArray([]);
    this.myForm.addControl('addresses', this.addressesForm);
  

子组件

@Component(
  selector: 'app-address-form',
  template: `
    <input [formControl]="addressForm.controls.addressLine1">
    <input [formControl]="addressForm.controls.city">
  `
)
export class AddressFormComponent implements OnInit 
  ...

  ngOnInit() 
    this.addressForm = this.formBuilder.group(
      addressLine1: [
        this.addressData.addressLine1,
        [ Validators.required ]
      ],
      city: [
        this.addressData.city
      ]
    );

    this.addressesForm.push(this.addressForm);
  

【问题讨论】:

查看Everything you need to know about the ExpressionChangedAfterItHasBeenCheckedError error文章 我在过去 24 小时内访问了该页面 5 次 ;)。仍然不确定如何解决它。 你引用的 plunker 没有错误 很奇怪。在禁用所有扩展程序的情况下,我在 Chrome 和 Firefox 中都遇到了错误。在 Plunker 和 localhost 中。添加了截图供参考。 我在使用 plunk 时遇到错误。我删除了地址表单组件中的验证([ Validators.required ]),错误消失了。不确定这些信息是否有用。 【参考方案1】:

要了解问题需要阅读Everything you need to know about the ExpressionChangedAfterItHasBeenCheckedError error 文章。

对于您的特定情况,问题是您在AppComponent 中创建一个表单并在DOM 中使用myForm.valid 插值。这意味着 Angular will run create and run updateRenderer function 用于更新 DOM 的 AppComponent。然后你使用子组件的ngOnInit 生命周期钩子将带有控件的子组添加到此表单:

export class AddressFormComponent implements OnInit 
  @Input() addressesForm;
  @Input() addressData;

  ngOnInit() 
    this.addressForm = this.formBuilder.group(
      addressLine1: [
        this.addressData.addressLine1,
        [ Validators.required ]   <-----------
      ]

    this.addressesForm.push(this.addressForm); <--------

控件变得无效,因为您没有提供初始值并且您指定了必需的验证器。因此整个表单变得无效,表达式myForm.valid 的计算结果为false。但是当 Angular 对AppComponent 运行更改检测时,它的评估结果为true。这就是错误所说的。

一个可能的解决方法是在开始时将表单标记为无效,因为您计划添加所需的验证器,但 Angular 似乎没有提供这种方法。您最好的选择可能是异步添加控件。事实上,这就是 Angular 在源代码中所做的:

const resolvedPromise = Promise.resolve(null);

export class NgForm extends ControlContainer implements Form 
  ...

  addControl(dir: NgModel): void 
    // adds controls asynchronously using Promise
    resolvedPromise.then(() => 
      const container = this._findContainer(dir.path);
      dir._control = <FormControl>container.registerControl(dir.name, dir.control);
      setUpControl(dir.control, dir);
      dir.control.updateValueAndValidity(emitEvent: false);
    );
  

所以你的情况是:

const resolvedPromise = Promise.resolve(null);

@Component(
   ...
export class AddressFormComponent implements OnInit 
  @Input() addressesForm;
  @Input() addressData;

  addressForm;

  ngOnInit() 
    this.addressForm = this.formBuilder.group(
      addressLine1: [
        this.addressData.addressLine1,
        [ Validators.required ]
      ],
      city: [
        this.addressData.city
      ]
    );

    resolvedPromise.then(() => 
       this.addressesForm.push(this.addressForm); <-------
    )
  

或者在AppComponent中使用一些变量来保存表单状态并在模板中使用它:

formIsValid

export class AppComponent implements OnInit 
  myForm: FormGroup;
  formIsValid = false;

  constructor(private formBuilder: FormBuilder) 

  ngOnInit() 
    this.myForm = this.formBuilder.group();
    this.myForm.statusChanges((status)=>
       formIsValid = status;
    )
  

【讨论】:

感谢非常详细的回答!欣赏它。 对于存档:第一种方法完美无缺。第二个没用。它只是将相同的错误移至另一个变量。还需要一个 .subscribe 来检查 statusChanges。 我们使用自己的嵌套表单实现,基于模板驱动的表单。但问题是一样的。添加或删除控件时,会出现错误。添加resolvedPromise,问题就解决了!谢谢@AngularInDepth.com @DHFW,很高兴我能帮上忙 第二种解决方案(在变量上保持valid 状态)对我有用,但由于某种原因我不得不用setTimeout() 包装它。谢谢【参考方案2】:
import ChangeDetectorRef from '@angular/core';
....

export class SomeComponent 

  form: FormGroup;

  constructor(private fb: FormBuilder,
              private ref: ChangeDetectorRef) 
    this.form = this.fb.group(
      myArray: this.fb.array([])
    );
  

  get myArray(): FormArray 
    return this.form.controls.myArray as FormArray;
  

  addGroup(): void 
    const newGroup = this.fb.group(
      prop1: [''],
      prop2: ['']
    );

    this.myArray.push(newGroup);
    this.ref.detectChanges();
  

【讨论】:

这绝对是一个被低估的答案...【参考方案3】:

我在 Angular 9 及更高版本的解决方案中遇到了相同的场景和相同的问题。我稍微调整了一下:在没有验证器的情况下同步添加控件并异步添加所需的验证器...因为我立即需要控件,否则我收到错误cannot find formControl ...

这是我基于上面接受的答案的解决方案:

const resolvedPromise = Promise.resolve(null);
@Component(
  selector: 'password-input',
  templateUrl: './password-input.component.html',
  styleUrls: ['./password-input.component.css']
)
export class PasswordInputComponent implements OnInit 
  @Input() parentFormGroup : FormGroup;
  @Input() controlName : string = 'password';
  @Input() placeholder : string = 'Password';
  @Input() label : string = null;

  ngOnInit(): void 
    this.parentFormGroup.addControl(this.controlName, new FormControl(null));
   
    resolvedPromise.then( () => 
      this.parentFormGroup.get(this.controlName).setValidators(Validators.required)
      this.parentFormGroup.get(this.controlName).updateValueAndValidity();
    );
  

【讨论】:

做了同样的事情,只是用 setTimeout()【参考方案4】:

按照 Max Koretskyi 的解决方案,我们可以在 ngOnInit 上使用 async/await 模式。

这是一个例子:

@Component(
  selector: 'my-sub-component',
  templateUrl: 'my-sub-component.component.html',
)
export class MySubComponent implements OnInit 
  @Input()
  form: FormGroup;

  async ngOnInit() 
    // Avoid ExpressionChangedAfterItHasBeenCheckedError
    await Promise.resolve(); 

    this.form.removeControl('config');
    
    this.form.addControl('config', new FormControl("", [Validator.required]));       
  

【讨论】:

以上是关于动态嵌套反应形式:ExpressionChangedAfterItHasBeenCheckedError的主要内容,如果未能解决你的问题,请参考以下文章

Angular 5-反应形式和动态形式之间的区别

动态键/值形式反应角度 5

primeNG p-multiSelect 具有动态反应形式设置值

使用 angular2 反应形式动态设置选择框值

如何使用验证编辑嵌套的反应式表单?

Mat Table 中的嵌套反应表单找不到控件