使用 *ngFor 和 ContentChildren 时出现 ExpressionChangedAfterItHasBeenCheckedError

Vla*_*yev 2 javascript exception angular

我有选项卡和选项卡组件:

tabs.component.ts:

@Component({
    selector: 'cl-tabs',
    template: `<ng-content></ng-content>`,
    styleUrls: ['tabs.component.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;
    }
}
Run Code Online (Sandbox Code Playgroud)

tab.component.ts:

@Component({
    selector: 'cl-tab',
    template: `<ng-content></ng-content>`,
    styleUrls: ['tab.component.scss'],
    changeDetection: ChangeDetectionStrategy.OnPush,
})
export class TabComponent {
    @Input() title: string;
    @Input() @HostBinding('class.is-active') active: boolean = false;
}
Run Code Online (Sandbox Code Playgroud)

app.component.html:

<cl-tabs>
    <cl-tab *ngFor="let item of items" [title]="item.name">
        <any-component [item]="item"></any-component>
    </cl-tab>
</tabs>
Run Code Online (Sandbox Code Playgroud)

当使用 *ngFor 创建选项卡时,会抛出 ExpressionChangedAfterItHasBeenCheckedError。

cdRef.detectChanges(),正如在许多情况下所建议的,没有帮助。

如果出现以下情况,错误就会消失:

  • 标签是静态创建的;或者
  • HostBinding 从“活动”字段中删除;或者
  • setTimeout 应用于 ngAfterContentInit。

为什么在前两种情况下错误会消失?在这种特殊情况下,有没有更好的 setTimeout 替代方案?

更新:在 plunker https://embed.plnkr.co/ZUOvf6NXCj2JOLTwbsYU/ 中重现错误

Max*_*kyi 5

要理解答案,您必须先阅读并理解这两篇文章:

让我们看看下面的App组件模板:

<cl-tabs>
  <cl-tab *ngFor="let item of items" [title]="item.name"></cl-tab>
</cl-tabs>
Run Code Online (Sandbox Code Playgroud)

在这里,您有内容投影和嵌入视图(由 创建ngFor)。
有两点是必须理解的:

  • 投影节点作为现有视图的一部分进行注册和存储,而不是它们投影到的视图
  • ngAfterContentInit触发之前检查嵌入的视图。

现在,请记住以上几点为您的特定设置

export class App {
  items = [
    {name: "1", value: 1},
    {name: "2", value: 2},
    {name: "3", value: 3},
  ];
}
Run Code Online (Sandbox Code Playgroud)

您可以对 AppComponent 视图的以下结构进行映像:

AppView.nodes: [
   clTabsComponentView
   ngForEmbeddedViews: [
      ngForEmbeddedView<{name: "1", value: 1}>
      ngForEmbeddedView<{name: "2", value: 2}>
      ngForEmbeddedView<{name: "3", value: 3}>
Run Code Online (Sandbox Code Playgroud)

当为 AppView 触发更改检测时,这里是操作顺序:

1) 检查 clTabsComponentView

2) 检查 ngForEmbeddedViews 中的所有视图。
这里active = false会记住每个视图的值。

3) 调用ngAfterContentInit生命周期钩子。
在这里你更新active = true. 所以在验证阶段 Angular 会检测差异并报告错误。

为什么在前两种情况下错误会消失?现在确定你的意思statically。如果您的意思是不使用,*ngFor则不会出现错误,因为每个 TabComponentView 将是子视图,而不是嵌入视图。并且在ngAfterContentChecked生命周期钩子之后处理子视图。

如果您删除,@HostBinding则 Angular 将无法检查组件,并且active不会记住值 - 因此不会进行验证和检查。

cdRef.detectChanges(),正如在许多情况下所建议的,没有帮助。

您需要在 AppComponent 上调用更改检测才能使其工作。