angular: ExpressionChangedAfterItHasBeenCheckedError with dynamic validators in a template driven composite form control

I’m submitting a…

[x] Bug report 

Current behavior

If you have a composite form control (think a <my-address [formControl]="address"></my-address> component), if the internal form has a dynamic validator (i.e. [required]="isUSA"), the control will throw an ExpressionChangedAfterItHasBeenCheckedError error when the validator (in this example, isUSA value) changes:

Error: ExpressionChangedAfterItHasBeenCheckedError: 
Expression has changed after it was checked. 
Previous value: 'ng-valid: true'. Current value: 'ng-valid: false'.

Expected behavior

There to not be an ExpressionChangedAfterItHasBeenCheckedError error.

Minimal reproduction of the problem with instructions

Click on the Toggle Requiredness button, and note in the console that there is an ExpressionChangedAfterItHasBeenCheckedError error. (The circular JSON error seems to be generated from StackBitz.)

Demo: https://stackblitz.com/edit/angular-d9rsnv?file=app%2Fmy-composite-control%2Fmy-composite-control.component.ts

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

Template driven forms are easier to use, and template-driven (aka directive) validators are easier to work with and conditionally add and remove. You can do this similarly with reactive forms and you don’t get this error.

Environment

Angular version: 5.2.8


Browser:
- [x] Firefox (desktop) version 59.0.3

Other information

Note that if you change the toggleRequiredness method to update the validators on the control, you don’t get the error (but the point of this bug is that it should work by using purely template-driven means). For example, remove the required attribute from the input in the demo link above and update the toggleRequiredness method to this:

  toggleRequiredness() {
    this.nameRequired = !this.nameRequired;

    const control = this.form.controls['name'];

    if (this.nameRequired) {
      control.setValidators([Validators.required]);
    } else {
      control.clearValidators();
    }

    control.updateValueAndValidity();
  }

Demo (with the above modification): https://stackblitz.com/edit/angular-hhgkje?file=app%2Fmy-composite-control%2Fmy-composite-control.component.ts

Alternatively, you can manually trigger this.cdr.detectChanges(); when updating the requiredness. The argument of this bug is that this sort of behavior shouldn’t be necessary with this kind of template-driven form.

About this issue

  • Original URL
  • State: open
  • Created 6 years ago
  • Reactions: 91
  • Comments: 60 (3 by maintainers)

Commits related to this issue

Most upvoted comments

We’re having this same issue and I know it’s not ideal but the only solution we’ve found is to wrap in a setTimeout setTimeout(() => { control.updateValueAndValidity(); })

For anyone still looking for the fix, this one from StackOverflow works for me: https://stackoverflow.com/a/45467987/662084

import {ChangeDetectorRef, AfterContentChecked} from '@angular/core';

export class MyComponent implements AfterContentChecked {

    constructor(private cdref: ChangeDetectorRef ) {}

    ngAfterContentChecked(): void {
        this.cdref.detectChanges();
    }

}

All credits to Richie Fredicson, the author of the answer on SO.

I tried every single possible solution, and after trying setTimeout, updateValueAndValidity and everything, @afrish solved my problem, thank you!

Original post: https://stackoverflow.com/a/45467987/662084

import { AfterContentChecked, ChangeDetectorRef } from '@angular/core'

export class MyComponent implements AfterContentChecked {

    constructor(private cdRef: ChangeDetectorRef ) {}

    ngAfterContentChecked(): void {
        this.cdRef.detectChanges()
    }

    ...
}

You can delay checking if the form is valid until everything has finished loading.

ngAfterViewInit() {
   this.pageLoaded = true;
}

isFormValid(): boolean {
	if (this.pageLoaded) {
		return this.formGroup.valid;
	} else {
		return false;
	}
}

Make a hidden element directly in form. Be sure to bind element to form and mark it required. <input [hidden]=“true” required=“true” formControlName=“yoyo”>

Solved !

Does this work for anyone, on the parent component please set: @Component({ changeDetection: ChangeDetectionStrategy.OnPush, })

Do not blindly add random lines you found in the internet to your code. The decision to start using OnPush strategy should be taken after reading all the limitations of this strategy.

One of them, for example, is that “children” components can’t override this strategy. So even if you don’t set changeDetection: ChangeDetectionStrategy.OnPush in components you use in your template, they will be switched to OnPush.

It’s not just some “magic” line - it will affect your app and it’s a sharp blade - you can do good things using it, but a sharp blade is dangerous.

Does this work for anyone, on the parent component please set: @Component({ changeDetection: ChangeDetectionStrategy.OnPush, })

I have a similar issue where I have a FormGroup that has a FormArray of FormsGroups ( a data table with Material Design table). It is a nested form with tables.

When I add a new row to it, I am actually updating that recordset with a new empty row, and then I clear the formArray and recreate the new formArray rows.

So before I click the Add Row button, the table has 12 rows and is valid, then I add a new row to it, we have 13 rows of which one is invalid. Now the whole form is invalid (which is true). When I went to Change Detection OnPush I had to create container component which passe in the data, so that the component rendering the table receives a Change. So by moving my FormArray create logic into the ngOnChanges now causes that error: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: 'ng-valid: true'. Current value: 'ng-valid: false'.

I have this bug, but set timeout helped

Please fix this. This bug can be reproduced and and hard to have a workaround for reactive form-based framework

After some investigation here is what I was doing that caused the issue: Created form. form is valid. Added component on screen that added form element. form is now invalid.

The act of adding a component should possibly be handled slightly differently. In my case I simply added that it was invalid if the field does not exist.

@new-mikha and @waratah solved about 90% of my issue, but I had to make a minor modification.

I had a form which was split between a parent and child component, and the child component would conditionally set [required] on the inputs based on “all fields have values or no fields have values”. The parent component has a submit button which is disabled if the form is invalid and enabled if the form is valid.

When the form was invalid (as it is in a pristine state based on form controls in the parent) the button would always be disabled and so requiring and unrequiring the child component fields was working (the 90%). However, if the rest of the form is valid (eg: none of the fields in the child have values) then the button is enabled. If I then enter a value in one of the fields of the child component I get the exception since a change event is triggering another change event after the parent has been checked.

So the workaround:

  1. Set the parent’s ChangeDetectionStrategy to OnPush
  2. Import ChangeDetectorRef in the constructor the parent
  3. Make Subject in the child component to notify the parent when to detect changes
  4. In the ngAfterViewInit() lifecycle hook of the parent, subscribe to the child’s subject via @ViewChild() and in the subscription call detectChanges() on the ChangeDetectorRef
  5. When you are changing the [required]ness of the form control in the child, call .next() on the subject

And it works. The submit button updates on blur now, which isn’t ideal I suppose but that’s a problem for future me.

Note that this will disable the usual change detection in the parent so you may need to call it manually depending on what is happening in the parent.

Any update on this?

After some investigation here is what I was doing that caused the issue: Created form. form is valid. Added component on screen that added form element. form is now invalid.

Thanks for the tip @waratah . I’ve got a similar issue, and the workaround in my case was to make the form invalid straight away when it’s created:

  • The validation I’ve got is just a ‘required’ attribute on an field inside the child component.
  • The parent FormGroup and FormControl for the input are created in the parent component.
  • The fix was to add Validators.Required to the FormControl constructor (in the parent component) I.e. now when the form group is created in the parent component, with the FormControl inside it, it’s invalid straight away.

The Angular version used is 6.1.2

While it is good to know that there are work-arounds like manual change detection this issue should be handled by the framework.

While a lot of members are calling “me too”, I’d like to point out that the “error” might be justified and the own code needs to be reviewed. I tracked down a ExpressionChangedAfterItHasBeenCheckedError on adding a Validator to the fact that the validator was added in an *ngIf expression, where it simply doesn’t belong. Changing the design to adding the validator using a @Directive made the design cleaner, and the error disappear more as a side-effect.

I use this solution with async/await pattern on ngOnInit: https://stackoverflow.com/a/68806251/4937858

@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]));       
  }
}

I am trying to avoid detectChanges and setTimeout and I found a way that did work.

I have 3 components. Component A relies on the formGroup in child component B, which has an array of formGroups from child component C. Putting this.formGroup.updateValueAndValidity(); in ngAfterViewInit on component C made it work. Having that in A or B gave me the error. Also remember to rerun updateValueAndValidity whenever you add any controls.

@afrish usually you see this in ngAfterViewInit.

Calling detectChanges will trigger another cycle of change detection (and then its subsequent check to see if anything changed between cycles). This has a negative performance impact, and while it’s often negligible, it’s not nothing and it can add up (if you have a lot of components on a page doing this, you end up with a lot of extra cycles and performance will be affected).

Putting it in ngAfterContentChecked does indeed get the error to go away in most cases, but because any time there is change, it’s gonna be running a lot of extra change detection, and this will have even more of a potential for negative performance.

@CWSpear … I can’t see any error on console.

any update ?

Reproduced on my application. I have a field that is sending enabled and disabled as a property on another field and this validator is triggering this error.

Angular CLI: 6.0.8 Node: 8.9.4 OS: darwin x64 Angular: 6.0.9

Chrome browser

Disappointed that this is still open after 6 years, and apparently no response from Angular?

Minimal steps here to reproduce:

describe('change detection test', () => {
  let fixture: ComponentFixture<TestComponent>;

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      imports: [
        TestComponent
      ]
    }).compileComponents();
  });

  it('should work', async () => {
    fixture = TestBed.createComponent(TestComponent);
    fixture.detectChanges();
    await fixture.whenStable();

    const childComponent = fixture.debugElement.query(By.directive(TestChildComponent)).componentInstance;
    childComponent.valueRequired = true;

    // Fails on this line with:
    //   Error: NG0100: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value for 'ng-valid': 'true'. Current value: 'false'. Expression location: TestComponent component.
    fixture.detectChanges();

    // Never gets to here
    expect(true).toBeTrue();
  });
});

@Component({
  standalone: true,
  imports: [
    FormsModule
  ],
  selector: 'app-test-child',
  template: `<input name="myField" [(ngModel)]="value" [required]="valueRequired"/>`,
  viewProviders: [
    {provide: ControlContainer, useExisting: NgForm}
  ]
})
class TestChildComponent {
  value = '';
  valueRequired: boolean = false;
}

@Component({
  standalone: true,
  imports: [
    FormsModule,
    TestChildComponent
  ],
  template: `<form><app-test-child></app-test-child></form>`
})
class TestComponent {
}

The issue is still valid. In my case we had a custom message which was displayed when error is present as a result of validation, but the formControl was changed dynamically using setValue method. Only one solution from the comments (updateValueAndValidity https://github.com/angular/angular/issues/23657#issuecomment-426058601) solved the problem but the app still prints the error to stack trace and it skews monitoring data.

This issue is still valid. in my case we had a custom form field, which produces this error if we use the [required] attribute with it.

For us, the solution was to put hte this.cdref.detectChanges(); in ngOnInit in the component which includes the said custom form field.

What’s the status on this? I know there’s a ton of other competing issues which also need addressing, but it feels like Reactive Forms have been sidelined for quite a few releases now, and this issue (along with others) desperately need some love!

Does altering the change detection strategy help? This ChangeDetectionStrategy.OnPush works in my example and doesn’t display the error.

Make a hidden element directly in form. Be sure to bind element to form and mark it required. <input [hidden]=“true” required=“true” formControlName=“yoyo”>

Solved !

This solution worked for me given by @snavtechnologies The main thing was to make form INVALID before we add dynamic controls. At the time of submitting form i removed validators for hidden element yoyo this.FormControl.yoyo.clearValidators(); this.FormControl.yoyo.updateValueAndValidity();

This problem got worse for me after upgrading from Angular 7 to Angular 8.

In Angular 7, I was able to work around the problem/error using setTimeout, but that no longer works with Angular 8.

I think the same issue exists with Reactive Forms as well, this seems to be the same bug, except the validator is not dynamic (but each CVA is a control which belongs to a FormArray): https://stackblitz.com/edit/angular-ivy-epkzdx?file=src/app/app.component.ts

Is there really no better way to address this than using workarounds? If it really is an issue with the validation that triggers an event change after the lifecycle has allready performed its checks (by what I gathered from the thread), there has to be some fundamental problems in the way reactive forms work that needs to be addressed. Dynamic and reactive forms are such a fundamental part of interactive applications nowadays that I don’t want to rely on hacks and workarounds for them.

@devilb0x You should not push directly on the controls array, use the FormArray itself instead. See the documentation here.

@KSoto that actually worked for me. My data was coming in after the form was created and so it would go from valid to invalid in a single cycle and then later a user could change the form by selecting something and also changing the validations. I just added the this.formGroup.updateValueAndValidity(); in the method where to form was modified and had to add ngAfterViewInit(): void { Promise.resolve().then(() => this.updateForm()); } when the form is initially built and all my errors went away. The Promise.resolve forces a microtask in zone.js and is much better than a setTimeout

i have the same issue: RegisterComponent.html:215 ERROR Error: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: ‘is-invalid: null’. Current value: ‘is-invalid: [object Object]’.

I made a custom validator:

validateIf(userValidator: (control: AbstractControl) => ValidationErrors | null, expression: () => boolean) {
    const validator = (control: AbstractControl): ValidationErrors | null => {
      if (expression()) {
        return userValidator(control);
      } else {
        return null;
      }
    };
    return validator;
  }

and passed it to my form control:

agencyName: ['', [
        this.validateIf(Validators.required,
          () => ((this.accountType)
            ? ((this.accountType.value)
              ? this.accountType.value.Key
              : null)
            : null) === AccountType.Agency
        ),
        this.validateIf(Validators.maxLength(environment.validations.agencyNameMaxLength),
          () => ((this.accountType)
            ? ((this.accountType.value)
              ? this.accountType.value.Key
              : null)
            : null) === AccountType.Agency
        )
      ]],

then this line in my template cause errors: [ngClass]="{'is-invalid': agencyName.errors}" in:

<input type="text" class="form-control" placeholder="Agency Name" formControlName="agencyName"
      [ngClass]="{'is-invalid': agencyName.errors}" />

My solution to my own code, which in point of my view is dirty and wrong:

STEP 1: Adding (Change) method (change)="onAccountTypeChanged()" to my account types

<select class="form-control select-place-holder" formControlName="accountType" (change)="onAccountTypeChanged()"
              [ngClass]="{'select-place-holder': util.isNullOrUndefined(accountType.value),
                          'is-invalid': accountType.errors && accountType.touched}">
          <option [ngValue]="null" [disabled]="true" [selected]="true">Type of Account</option>
          <option *ngFor="let account of (accountTypes)" [ngValue]="account">{{account.Value}}</option>
      </select>

Step 2: Create new variable to use indirect accessing to form values, and fill it in change method: selectedAccountType: KeyValuePair<number, string> = null;

STEP 3: Write change method, in a way that it set our variables value in hope that change performs later, not at the form rendering time.

  onAccountTypeChanged() {
      this.selectedAccountType = this.accountType ? this.accountType.value : null;
  }

STEP 4: anchor our validation to this newly value instead of directly reading from form data:

agencyName: ['', [
        this.validateIf(Validators.required,
          () => ((this.selectedAccountType)
              ? this.accountType.value.Key
              : null) === AccountType.Agency
        ),
        this.validateIf(Validators.maxLength(environment.validations.agencyNameMaxLength),
          () => ((this.selectedAccountType)
              ? this.accountType.value.Key
              : null) === AccountType.Agency
        )
      ]],

STEP 5: TESTING: Still Failed 😐 so the (Change)=“…” method perform in about same time as the form.get(‘x’).value.

STEP 6: listen to @joeylgutierrez

We’re having this same issue and I know it’s not ideal but the only solution we’ve found is to wrap in a setTimeout setTimeout(() => { control.updateValueAndValidity(); })

i wouldn’t be able to do this, if i didn’t perform last 4 steps. So here i add setTimeout

onAccountTypeChanged() {
    setTimeout(() => {
      this.selectedAccountType = this.accountType ? this.accountType.value : null;
    });
  }

STEP 7: TESTING: Everything work as it supposed to.

Although it worked, i still believe the form way of behaving should changes.