My component has styles that depend on current datetime. In my component I've got the following function.
private fontColor( dto : Dto ) : string {
// date d'exécution du dto
let dtoDate : Date = new Date( dto.LastExecution );
(...)
let color = "hsl( " + hue + ", 80%, " + (maxLigness - lightnessAmp) + "%)";
return color;
}
lightnessAmp
is calculated from the current datetime. The color changes if dtoDate
is in the last 24 hours.
The exact error is the following:
Expression has changed after it was checked. Previous value: 'hsl( 123, 80%, 49%)'. Current value: 'hsl( 123, 80%, 48%)'
I know the exception appear in development mode only at the moment the value is checked. If the checked value is different of the updated value, the exception is thrown.
So I tried to update the current datetime at each lifecycle in the following hook method to prevent the exception:
ngAfterViewChecked()
{
console.log( "! changement de la date du composant !" );
this.dateNow = new Date();
}
...but without success.
This question is related to
angular
typescript
time
styles
components
TL;DR
ngAfterViewInit() {
setTimeout(() => {
this.dateNow = new Date();
});
}
Although this is a workaround, sometimes it's really hard to solve this issue in any nicer way, so don't blame yourself if you are using this approach. That's okay.
Examples: The initial issue [link], Solved with setTimeout()
[link]
How to avoid
In general this error usually happens after you add somewhere (even in parent/child components) ngAfterViewInit
. So first question is to ask yourself - can I live without ngAfterViewInit
? Perhaps you move the code somewhere ( ngAfterViewChecked
might be an alternative).
Example: [link]
Also
Also async stuff in ngAfterViewInit
that affects DOM might cause this. Also can be solved via setTimeout
or by adding the delay(0)
operator in the pipe:
ngAfterViewInit() {
this.foo$
.pipe(delay(0)) //"delay" here is an alternative to setTimeout()
.subscribe();
}
Example: [link]
Nice Reading
Good article about how to debug this and why it happens: link
As mentioned by @leocaseiro on github issue.
I found 3 solutions for those who are looking for easy fixes.
1) Moving from
ngAfterViewInit
tongAfterContentInit
2) Moving to
ngAfterViewChecked
combined withChangeDetectorRef
as suggested on #14748 (comment)3) Keep with ngOnInit() but call
ChangeDetectorRef.detectChanges()
after your changes.
In our case we FIXED by adding changeDetection into the component and call detectChanges() in ngAfterContentChecked, code as follows
@Component({
selector: 'app-spinner',
templateUrl: './spinner.component.html',
styleUrls: ['./spinner.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class SpinnerComponent implements OnInit, OnDestroy, AfterContentChecked {
show = false;
private subscription: Subscription;
constructor(private spinnerService: SpinnerService, private changeDedectionRef: ChangeDetectorRef) { }
ngOnInit() {
this.subscription = this.spinnerService.spinnerState
.subscribe((state: SpinnerState) => {
this.show = state.show;
});
}
ngAfterContentChecked(): void {
this.changeDedectionRef.detectChanges();
}
ngOnDestroy() {
this.subscription.unsubscribe();
}
}
Although there are many answers already and a link to a very good article on change detection, I wanted to give my two cents here. I think the check is there for a reason so I thought about the architecture of my app and realized that the changes in the view can be dealt with by using BehaviourSubject
and the correct lifecycle hook. So here's what I did for a solution.
So I ended up getting the underlying JavaScript class, and need to initialize my own calendar header for the component. That requires the ViewChild
to be rendered befor my parent is rendered, which is not the way Angular works. This is why I wrapped the value I need for my template in a BehaviourSubject<View>(null)
:
calendarView$ = new BehaviorSubject<View>(null);
Next, when I can be sure the view is checked, I update that subject with the value from the @ViewChild
:
ngAfterViewInit(): void {
// ViewChild is available here, so get the JS API
this.calendarApi = this.calendar.getApi();
}
ngAfterViewChecked(): void {
// The view has been checked and I know that the View object from
// fullcalendar is available, so emit it.
this.calendarView$.next(this.calendarApi.view);
}
Then, in my template, I just use the async
pipe. No hacking with change detection, no errors, works smoothly.
Please don't hesitate to ask if you need more details.
I got that error because I declared a variable and later wanted to
changed it's value using ngAfterViewInit
export class SomeComponent {
header: string;
}
to fix that I switched from
ngAfterViewInit() {
// change variable value here...
}
to
ngAfterContentInit() {
// change variable value here...
}
Her you go two solutions!
For this solution, you'r basically telling angular:
Stop checking for changes; i'll do it only when i know is necessary
The quick fix:
Modify your component so it'll use ChangeDetectionStrategy.OnPush
@Component({
selector: 'app-child',
templateUrl: './child.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ChildComponent implements OnInit {
// ...
}
With this, things don’t seem to work anymore. That's because from now on you'll have to make Angular call the detectChanges()
manually.
this.cdr.detectChanges();
Here's a link that helped me understand ChangeDetectionStrategy right: https://alligator.io/angular/change-detection-strategy/
Here is a small extract from tomonari_t answer about the causes for this error, I've tried to include only the parts that helped me to understand this.
The full article shows real code examples about every point shown here.
The root cause is angular lifecycle's:
After each operation Angular remembers what values it used to perform an operation. They are stored in the oldValues property of the component view.
After the checks have been done for all components Angular then starts the next digest cycle but instead of performing operations it compares the current values with the ones it remembers from the previous digest cycle.
The following operations are the being checked at digest cycle's:
check that values passed down to the child components are the same as the values that would be used to update properties of these components now.
check that values used to update the DOM elements are the same as the values that would be used to update these elements now perform the same.
checks for all child components
And so, the error is thrown when the compared values are different., blogger Max Koretskyi stated:
The culprit is always the child component or a directive.
And finally here are some real world samples that usually cause this error:
Every sample can be found here (plunkr), in my case the problem was a dynamic component instantiation.
Also, by my own experience i strongly recommend everyone to avoid the setTimeout
solution, in my case caused an "almost" infinite loop (21 calls which i'm not willing to show you how to provoke them),
I would recommend to always keep in mind Angular life-cycle's so you can take in account how they would be affected every time you modify another component's value. With this error Angular is telling you:
You're maybe doing this the wrong way, are you sure you're right?
The same blog also says:
Often, the fix is to use the right change detection hook to create a dynamic component
A short guide for me is to consider at least the following two things while coding (i'll try to complement it over time):
@Input
and @Output
directives try to avoid triggering lyfecycle changes unless the component is completely initialized.this.cdr.detectChanges();
they can trigger more errors, specially when you'r dealing with a lot of dynamic datathis.cdr.detectChanges();
is mandatory make sure that the variables (@Input, @Output, etc
) being used are filled/initialized at right detection hook (OnInit, OnChanges, AfterView, etc
)If you want to fully understand Angular Life Hook i recommend you to read the official documentation here:
A small work around I used many times
Promise.resolve(data).then(() => {
console.log( "! changement de la date du composant !" );
this.dateNow = new Date();
this.cdRef.detectChanges();
});
_x000D_
I think the best and cleanest solution you can imagine is this:
@Component( {
selector: 'app-my-component',
template: `<p>{{ myData?.anyfield }}</p>`,
styles: [ '' ]
} )
export class MyComponent implements OnInit {
private myData;
constructor( private myService: MyService ) { }
ngOnInit( ) {
/*
async .. await
clears the ExpressionChangedAfterItHasBeenCheckedError exception.
*/
this.myService.myObservable.subscribe(
async (data) => { this.myData = await data }
);
}
}
Tested with Angular 5.2.9
Move your code from ngAfterViewInit to ngAfterContentInit
Use a default form value to avoid the error.
Instead of using the accepted answer of applying detectChanges() in ngAfterViewInit() (which also solved the error in my case), I decided instead to save a default value for a dynamically required form field, so that when the form is later updated, it's validity is not changed if the user decides to change an option on the form that would trigger the new required fields (and cause the submit button to be disabled).
This saved a tiny bit of code in my component, and in my case the error was avoided altogether.
Source: Stackoverflow.com