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.)
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)
Links to this issue
Commits related to this issue
- fix ExpressionChangedAfterItHasBeenCheckedError - https://github.com/angular/angular/issues/23657#issuecomment-624127231 — committed to camueller/SmartApplianceEnabler by camueller 3 years ago
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
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!You can delay checking if the form is valid until everything has finished loading.
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 !
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 toOnPush
.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 thengOnChanges
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:
ChangeDetectionStrategy
toOnPush
ChangeDetectorRef
in the constructor the parentngAfterViewInit()
lifecycle hook of the parent, subscribe to the child’s subject via@ViewChild()
and in the subscription calldetectChanges()
on theChangeDetectorRef
[required]
ness of the form control in the child, call.next()
on the subjectAnd 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?
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 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
I am trying to avoid
detectChanges
andsetTimeout
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();
inngAfterViewInit
on component C made it work. Having that in A or B gave me the error. Also remember to rerunupdateValueAndValidity
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:
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();
inngOnInit
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.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 addngAfterViewInit(): 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 setTimeoutMy 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 typesStep 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.
STEP 4: anchor our validation to this newly value instead of directly reading from form data:
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
i wouldn’t be able to do this, if i didn’t perform last 4 steps. So here i add
setTimeout
STEP 7: TESTING: Everything work as it supposed to.
Although it worked, i still believe the form way of behaving should changes.