繁体   English   中英

使用 *ngFor 和 ContentChildren 时出现 ExpressionChangedAfterItHasBeenCheckedError

[英]Got ExpressionChangedAfterItHasBeenCheckedError when using *ngFor and ContentChildren

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

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;
    }
}

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;
}

app.component.html:

<cl-tabs>
    <cl-tab *ngFor="let item of items" [title]="item.name">
        <any-component [item]="item"></any-component>
    </cl-tab>
</tabs>

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

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

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

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

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

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

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

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

<cl-tabs>
  <cl-tab *ngFor="let item of items" [title]="item.name"></cl-tab>
</cl-tabs>

这里有内容投影和嵌入视图(由ngFor创建)。
有两点是必不可少的:

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

现在,请特别注意以上几点

export class App {
  items = [
    {name: "1", value: 1},
    {name: "2", value: 2},
    {name: "3", value: 3},
  ];
}

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

AppView.nodes: [
   clTabsComponentView
   ngForEmbeddedViews: [
      ngForEmbeddedView<{name: "1", value: 1}>
      ngForEmbeddedView<{name: "2", value: 2}>
      ngForEmbeddedView<{name: "3", value: 3}>

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

1)检查clTabsComponentView

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

3)调用ngAfterContentInit生命周期钩子。
在这里更新active = true 因此,在验证阶段,Angular将检测差异并报告错误。

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

如果删除@HostBinding则Angular无需检查组件,并且不会记住active值 - 因此无需验证,也无需检查。

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

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

我来得太晚了。 无论如何。

我确实添加了一个空模板来为 ngTemplateOutlet 提供一个值,同时每个模板都准备好了。

<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>

编辑

我在模板中使用了一个简单的 OR 运算符。 这里有趣的事情是:为什么此代码段在应用程序流程中稍后由 Angular 应用程序呈现时可以正常工作,而不是在窗口重新加载洞 Angular 应用程序时。 在第二种情况下,Angular 似乎直接使用 #placeholder 而不是 templateOutlet。

问题是你在通过角度变化检测检查后正在改变价值。 这是问题所在:

ngAfterContentInit(): void {
    if (0 < this.tabs.length) {
        this.activate(this.tabs.first);
    }
}

setTimeout将是创建macrotask的最佳解决方案。 另一种解决方案是再次触发更改检测,但这将为您的整个应用程序调用它。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM