最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - Got ExpressionChangedAfterItHasBeenCheckedError when using *ngFor and ContentChildren - Stack Overflow

programmeradmin2浏览0评论

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
 |  Show 2 more ments

3 Answers 3

Reset to default 6

To 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 the ngAfterContentChecked 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.

与本文相关的文章

发布评论

评论列表(0)

  1. 暂无评论