Expression ___ has changed after it was checked
Why is the component in this simple plunk
@Component({
selector: 'my-app',
template: `<div>I'm {{message}} </div>`,
})
export class App {
message:string = 'loading :(';
ngAfterViewInit() {
this.updateMessage();
}
updateMessage(){
this.message = 'all done loading :)'
}
}
throwing:
EXCEPTION: Expression 'I'm {{message}} in App@0:5' has changed after it was checked. Previous value: 'I'm loading :( '. Current value: 'I'm all done loading :) ' in [I'm {{message}} in App@0:5]
when all I'm doing is updating a simple binding when my view is initiated?
Solution 1:
As stated by drewmoore, the proper solution in this case is to manually trigger change detection for the current component. This is done using the detectChanges()
method of the ChangeDetectorRef
object (imported from angular2/core
), or its markForCheck()
method, which also makes any parent components update. Relevant example:
import { Component, ChangeDetectorRef, AfterViewInit } from 'angular2/core'
@Component({
selector: 'my-app',
template: `<div>I'm {{message}} </div>`,
})
export class App implements AfterViewInit {
message: string = 'loading :(';
constructor(private cdr: ChangeDetectorRef) {}
ngAfterViewInit() {
this.message = 'all done loading :)'
this.cdr.detectChanges();
}
}
Here are also Plunkers demonstrating the ngOnInit, setTimeout, and enableProdMode approaches just in case.
Solution 2:
First, note that this exception will only be thrown when you're running your app in dev mode (which is the case by default as of beta-0): If you call enableProdMode()
when bootstrapping the app, it won't get thrown (see updated plunk).
Second, don't do that because this exception is being thrown for good reason: In short, when in dev mode, every round of change detection is followed immediately by a second round that verifies no bindings have changed since the end of the first, as this would indicate that changes are being caused by change detection itself.
In your plunk, the binding {{message}}
is changed by your call to setMessage()
, which happens in the ngAfterViewInit
hook, which occurs as a part of the initial change detection turn. That in itself isn't problematic though - the problem is that setMessage()
changes the binding but does not trigger a new round of change detection, meaning that this change won't be detected until some future round of change detection is triggered somewhere else.
The takeaway: Anything that changes a binding needs to trigger a round of change detection when it does.
Update in response to all the requests for an example of how to do that: @Tycho's solution works, as do the three methods in the answer @MarkRajcok pointed out. But frankly, they all feel ugly and wrong to me, like the sort of hacks we got used to leaning on in ng1.
To be sure, there are occasional circumstances where these hacks are appropriate, but if you're using them on anything more than a very occasional basis, it's a sign that you're fighting the framework rather than fully embracing its reactive nature.
IMHO, a more idiomatic, "Angular2 way" of approaching this is something along the lines of: (plunk)
@Component({
selector: 'my-app',
template: `<div>I'm {{message | async}} </div>`
})
export class App {
message:Subject<string> = new BehaviorSubject('loading :(');
ngAfterViewInit() {
this.message.next('all done loading :)')
}
}
Solution 3:
ngAfterViewChecked()
worked for me:
import { Component, ChangeDetectorRef } from '@angular/core'; //import ChangeDetectorRef
constructor(private cdr: ChangeDetectorRef) { }
ngAfterViewChecked(){
//your code to update the model
this.cdr.detectChanges();
}
Solution 4:
I fixed this by adding ChangeDetectionStrategy from angular core.
import { Component, ChangeDetectionStrategy } from '@angular/core';
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'page1',
templateUrl: 'page1.html',
})
Solution 5:
Can't you use ngOnInit
because you just changing the member variable message
?
If you want to access a reference to a child component @ViewChild(ChildComponent)
, you indeed need to wait for it with ngAfterViewInit
.
A dirty fix is to call the updateMessage()
in the next event loop with e.g. setTimeout.
ngAfterViewInit() {
setTimeout(() => {
this.updateMessage();
}, 1);
}