I have tabs and tab ponents:
tabsponent.ts:
@Component({
selector: 'cl-tabs',
template: `<ng-content></ng-content>`,
styleUrls: ['tabsponent.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class TabsComponent implements AfterContentInit {
@ContentChildren(TabComponent) tabs: QueryList<TabComponent>;
ngAfterContentInit(): void {
if (0 < this.tabs.length) {
this.activate(this.tabs.first);
}
}
activate(activatedTab: TabComponent) {
this.tabs.forEach(tab => tab.active = false);
activatedTab.active = true;
}
}
tabponent.ts:
@Component({
selector: 'cl-tab',
template: `<ng-content></ng-content>`,
styleUrls: ['tabponent.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class TabComponent {
@Input() title: string;
@Input() @HostBinding('class.is-active') active: boolean = false;
}
appponent.html:
<cl-tabs>
<cl-tab *ngFor="let item of items" [title]="item.name">
<any-ponent [item]="item"></any-ponent>
</cl-tab>
</tabs>
When tabs are created using *ngFor an ExpressionChangedAfterItHasBeenCheckedError is thrown.
cdRef.detectChanges(), as suggested in many cases, doesn't help.
Error dissapears if:
- tabs are created statically; or
- HostBinding is removed from "active" field; or
- setTimeout is applied to ngAfterContentInit.
Why does the error dissapear in the first two cases? Is there any better alternative to setTimeout in that particular case?
UPDATE: Reproduced the error in plunker /
I have tabs and tab ponents:
tabs.ponent.ts:
@Component({
selector: 'cl-tabs',
template: `<ng-content></ng-content>`,
styleUrls: ['tabs.ponent.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class TabsComponent implements AfterContentInit {
@ContentChildren(TabComponent) tabs: QueryList<TabComponent>;
ngAfterContentInit(): void {
if (0 < this.tabs.length) {
this.activate(this.tabs.first);
}
}
activate(activatedTab: TabComponent) {
this.tabs.forEach(tab => tab.active = false);
activatedTab.active = true;
}
}
tab.ponent.ts:
@Component({
selector: 'cl-tab',
template: `<ng-content></ng-content>`,
styleUrls: ['tab.ponent.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class TabComponent {
@Input() title: string;
@Input() @HostBinding('class.is-active') active: boolean = false;
}
app.ponent.html:
<cl-tabs>
<cl-tab *ngFor="let item of items" [title]="item.name">
<any-ponent [item]="item"></any-ponent>
</cl-tab>
</tabs>
When tabs are created using *ngFor an ExpressionChangedAfterItHasBeenCheckedError is thrown.
cdRef.detectChanges(), as suggested in many cases, doesn't help.
Error dissapears if:
- tabs are created statically; or
- HostBinding is removed from "active" field; or
- setTimeout is applied to ngAfterContentInit.
Why does the error dissapear in the first two cases? Is there any better alternative to setTimeout in that particular case?
UPDATE: Reproduced the error in plunker https://embed.plnkr.co/ZUOvf6NXCj2JOLTwbsYU/
Share Improve this question edited Aug 11, 2017 at 8:06 Vladislav Nikolayev asked Aug 9, 2017 at 13:05 Vladislav NikolayevVladislav Nikolayev 332 silver badges5 bronze badges 7- hey, what is the exact wording of the error? that's a quite plex setup – Max Koretskyi Commented Aug 10, 2017 at 9:18
- @Maximus ERROR Error: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: 'false'. Current value: 'true'. – Vladislav Nikolayev Commented Aug 10, 2017 at 10:38
- I couldn't reproduce the problem. The setup is pretty plex. If you setup a plunker with the reproducible problem I'll take a look – Max Koretskyi Commented Aug 10, 2017 at 15:35
- @Maximus Updated question with a link to plunker with the error. – Vladislav Nikolayev Commented Aug 11, 2017 at 8:07
- tabs are created statically - what do you mean statically? – Max Koretskyi Commented Aug 11, 2017 at 9:00
3 Answers
Reset to default 6To understand the answer you have to read and understand these two articles first:
- Everything you need to know about change detection in Angular
- Everything you need to know about the
ExpressionChangedAfterItHasBeenCheckedError
error
Let's look at the following template of App
ponent:
<cl-tabs>
<cl-tab *ngFor="let item of items" [title]="item.name"></cl-tab>
</cl-tabs>
Here you have content projection and embedded views (created by ngFor
).
There are two points that are essential to understand:
- projected nodes are registered and stored as part of the existing view, not the view they are projected into
- embedded views are checked before the
ngAfterContentInit
is triggered.
Now, bearing the above points in mind for you particular setup
export class App {
items = [
{name: "1", value: 1},
{name: "2", value: 2},
{name: "3", value: 3},
];
}
You can imaging the following structure of the AppComponent view:
AppView.nodes: [
clTabsComponentView
ngForEmbeddedViews: [
ngForEmbeddedView<{name: "1", value: 1}>
ngForEmbeddedView<{name: "2", value: 2}>
ngForEmbeddedView<{name: "3", value: 3}>
And when change detection is triggered for the AppView here is the order of operations:
1) Check clTabsComponentView
2) Check all views in ngForEmbeddedViews.
Here the value active = false
is remembered for each view.
3) Call ngAfterContentInit
lifecycle hook.
Here you update the active = true
. So during verification phase Angular will detect the difference and report the error.
Why does the error dissapear in the first two cases? Now sure what you mean by
statically
. If you mean not using*ngFor
then there will be no error since then each TabComponentView will be child view, not an embedded view. And child views are processed after thengAfterContentChecked
lifecycle hook.
If you remove @HostBinding
then there's nothing for Angular to check in the ponent and active
value is not remembered - thus no verification and no check.
cdRef.detectChanges(), as suggested in many cases, doesn't help.
You need to call change detection on the AppComponent for this to work.
I'm so late. Anyways.
I did add an empty template to provide to ngTemplateOutlet a value meanwhile de each template is ready.
<div class="stepper--stages">
<div class="stepper--single-stage" *ngFor="let stage of stages">
<ng-container [ngTemplateOutlet]="stage.templateRef || placeholder"></ng-container>
</div>
<ng-template #placeholder></ng-template>
</div>
Edit:
I'm using a simple OR operator in the template. The interesting thing here is: Why this snippet works properly when it has been rendered by the Angular app later in the app's flow vs when the hole Angular app is reloaded by the window. In the second case, Angular seems like it's taking the #placeholder directly instead templateOutlet.
The problem is you are changing value after they have been checked by angulars changedetection. Here is the problem:
ngAfterContentInit(): void {
if (0 < this.tabs.length) {
this.activate(this.tabs.first);
}
}
setTimeout
would be the best solution as it creates a macrotask. An other solution would be to trigger changedetection again, but this will call it for your whole application.