【问题标题】:Got ExpressionChangedAfterItHasBeenCheckedError when using *ngFor and ContentChildren使用 *ngFor 和 ContentChildren 时出现 ExpressionChangedAfterItHasBeenCheckedError
【发布时间】:2018-01-17 09:28:07
【问题描述】:

我有标签和标签组件:

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/ 中的错误

【问题讨论】:

  • 嘿,错误的确切措辞是什么?这是一个相当复杂的设置
  • @Maximus ERROR 错误:ExpressionChangedAfterItHasBeenCheckedError:表达式在检查后已更改。以前的值:“假”。当前值:“真”。
  • 我无法重现该问题。设置相当复杂。如果您设置一个具有可重现问题的 plunker,我会看看
  • @Maximus 更新了问题,其中包含指向错误的 plunker 的链接。
  • 标签是静态创建的 - 静态是什么意思?

标签: javascript angular exception


【解决方案1】:

我来晚了。无论如何。

我确实添加了一个空模板来为 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 运算符。这里有趣的是:为什么这个 sn-p 在应用程序流中稍后由 Angular 应用程序呈现时与当窗口重新加载洞 Angular 应用程序时正常工作。在第二种情况下,Angular 似乎直接使用 #placeholder 而不是 templateOutlet。

【讨论】:

  • 注意:当包含此 sn-p 的路由在您的应用程序流程中稍后被调用时,它可以正常工作。如果这个 sn-p 在窗口重新加载后立即呈现,那么 Angular 将使用 #placeholder 而不是 ngTemplateOutlet。
  • 很好的解决方案,它有效,但你能解释一下吗?
  • @AndrzejKrawczuk 我编辑了我的答案。我不知道为什么这在我上面解释的两种情况下会有所不同。我明确知道这与 Angular 的渲染流程有关...
【解决方案2】:

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

我们看下面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 上调用更改检测才能使其正常工作。

【讨论】:

【解决方案3】:

问题是您在通过 angulars changedetection 检查后更改值。问题来了:

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

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

【讨论】:

  • 感谢您的回答!我仍然不明白为什么在前两种情况下使用静态选项卡或没有主机绑定时错误会消失。你对此有什么线索吗?至于变化检测,你的意思是运行 cdRef.detectChanges() 吗?
猜你喜欢
  • 2018-02-03
  • 1970-01-01
  • 2020-09-02
  • 1970-01-01
  • 2019-05-14
  • 1970-01-01
  • 1970-01-01
  • 2019-01-26
  • 1970-01-01
相关资源
最近更新 更多