angular: Calling setValidators on a form control when control is nested as a component in form causes ExpressionChangedAfterItHasBeenCheckedError

I’m submitting a …


[ ] Regression (behavior that used to work and stopped working in a new release)
[ X] Bug report 
[ ] Feature request
[ ] Documentation issue or request
[ ] Support request => Please do not submit support request here, instead see https://github.com/angular/angular/blob/master/CONTRIBUTING.md#question

Current behavior

Calling setValidators on a form control when control is nested as a component in form causes ExpressionChangedAfterItHasBeenCheckedError. Otherwise nesting formControls in components seems to work fine.

Expected behavior

When calling myform.controls[“my_control”].setValidators([Validators.required]); in a nested component of a reactive form I would expect no errors to occur, especially ExpressionChangedAfterItHasBeenCheckedError

Minimal reproduction of the problem with instructions

What is the motivation / use case for changing the behavior?

Please tell us about your environment


Angular version: ~4.2.0

Still an issue

Browser:
- [ X] Chrome (desktop) version XX
- [ ] Chrome (Android) version XX
- [ ] Chrome (iOS) version XX
- [ ] Firefox version XX
- [ ] Safari (desktop) version XX
- [ ] Safari (iOS) version XX
- [ ] IE version XX
- [ ] Edge version XX
 
For Tooling issues:
- Node version: 6.11.0 
- Platform: linux

Others:

About this issue

  • Original URL
  • State: open
  • Created 7 years ago
  • Reactions: 18
  • Comments: 25 (2 by maintainers)

Most upvoted comments

We’re experiencing a related issue: ExpressionChangedAfterItHasBeenCheckedError is always thrown for a very simple reactive form, if the input is on a lower template hierarchy level (defined by *ngFor/*ngIf etc) than the code accessing the form’s validity:

<form [name]="formName"
      [formGroup]="formGroup">
  <div *ngIf="true">
    <input formControlName="name"
           required
           placeholder="Please enter you name">
  </div>
</form>
<div *ngIf="formGroup.valid">
  Name is required.
</div>

Here’s a working plunkr.

The reason for the error being thrown is that change detection evaluates the *ngIf of the error div first before it evaluates the input’s validity, as the input is one level lower in terms of template hierarchy.

The workaround is to bring the error div onto the same level as the input by adding just another <div *ngIf="true"></div> around it (as mentioned above).

Work around my issue like this
Promise.resolve().then(()=>{ this.control.setValidators(this.buildValidators()); this.control.updateValueAndValidity(); });

I ran change detection manually in the ngAfterViewInit lifecycle hook of the parent component which did the trick (for me).

ngAfterViewInit() {
	this.cdRef.detectChanges();
}

The reason is that that hook is executed after the view AND the child views are initialized and thus the validators from the child component should be in place at that point.

Just stumbled upon this issue and it is reaaaaaally nasty one, I know that it will not matter in production but in dev env is really a show stopper.

A workaround for us was setting the component that holds the form to use OnPush detection strategy.

@renehamburger thank you so much 🙏 for clarifying that. I’ve encountered that issue plenty of times thinking I was doing something wrong and turns out… not really (?). At least from the point of view of CD.

It turns out this is happening for one nested form component where it wasn’t happening before nesting. For instance I’m passing in the formGroup via an input. What’s special about this component is that is makes use of ngDefaultControl [(ngModel)]=“rE.formattedDate” and when I set [Validators.required] in the parent form and change the input value the feedback loop starts. For now I’ve found a temporary fix by putting *ngIf=“true” on the nested component which contains the component using ngDefaultControl and I can still use the validators. I think it’s interesting this worked when not nested. I wonder if I should rewrite the component to not use ngDefaultControl [(ngModel)] or if this is even a bug. BTW this seems to complicated to put in a plunker but I’ll take a look to see if that’s feasible.

this error poped after upgrade to ivy

ExpressionChangedError on form.valid with following structure

`<custom-comp formGroup=“form” [isValid]=“(form.statusChanges | async) == ‘VALID’”>

<nested1 formControlName="nested1></nested1>

<nested2 formControlName="nested2></nested2>

</custom-comp>

<button [disabled]=“!((form.statusChanges | async) == ‘VALID’) || loading”

        (click)="submit('default')">

  <i [class]="loading ? 'fa fa-spinner fa-spin' : 'fa fa-check'"></i>buchen</button>`

workaround: replace form.valid with (form.statusChanges | async) == ‘VALID’

Just to add what I’m seeing from my tests, I was only seeing the issue if the form’s validity changed below the 2nd level of components, i.e. the 3rd level or lower.

<app-component>
   <form [formGroup]="myForm">
      <child1 [parentForm]="myForm">
       // ngOnInit() { this.parentForm.addControl('child1', new FormControl('')); }
            <child2 [parentForm]="myForm">
             // ngOnInit() {this.parentForm.addControl('child2', new FormControl('', Validators.required));}
            </child2>
      </child1>
   </form>
</app-component>

Here <child1> would add a control with no validators, so the form is valid. And then <child2> adds a control with an empty string, and the required validator, making the form invalid. This will cause the ExpressionChangedAfterItHasBeenCheckedError. If I instead made <child2> a sibling to <child1>, it does not throw the error when form.valid changes to false. I was able to use the *ngIf="true" workaround in this scenario by placing it around <child1>

<div *ngIf="true">
    <child1>
         <child2></child2>
    </child1>
</div>

Facing same issue. Here’s a very simple repro that only uses nested reactive form and ng-if. https://plnkr.co/edit/GrvjN3sJ05RSNXiSY8lo

I am facing the same issue with the nested form components. Please let me know if their is any updates on this issue.