根据视口更改 ngFor 中的类名 - expressionChangedAfterItHasBeenCheckedError

问题描述

在左侧,有一个带有类别标题的导航列表,在右侧,我列出了类别。列表中的标题可以根据右侧显示的类别“突出显示”。
1- 用户可以点击列表中的一个类别标题来滚动到它,现在点击的标题将被“突出显示”。
2- 如果用户滚动到其相应的类别部分,标题可能会被“突出显示”。
“突出显示”类别标题只需将 faq__category-m-heading w--current 分配给它的类即可完成。未突出显示标题具有 faq__category-m-heading 类。

HTML

//Left
<div class="faq__category-menu">

        <a *ngFor="let category of disPLAYED_CATEGORIES" (click)="scrolltoElement(category.FAQ_CATEGORY_ID)"
           [ngClass]="getClass(category.FAQ_CATEGORY_ID)">{{category.FAQ_CATEGORY_TITLE}}</a>

</div>

//Right
<div *ngFor="let category of disPLAYED_CATEGORIES" id="{{category.FAQ_CATEGORY_ID}}"
        class="faq__category-wrapper food">
        <h2 class="faq__category-heading">{{category.FAQ_CATEGORY_TITLE}}</h2>
//nonn relevant code section here
</div>

打字稿

 scrolltoElement($elementId): void {
    this.currentID = $elementId;  //I was trying another method here
    let el = document.getElementById($elementId);
    el.scrollIntoView({ behavior: "smooth",block: "start",inline: "nearest" });
  } 

isInViewport(elem) {
    var bounding = elem.getBoundingClientRect();
    return (
      bounding.top >= 0 &&
      bounding.left >= 0 &&
      bounding.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
      bounding.right <= (window.innerWidth || document.documentElement.clientWidth)
    );
  };

getClass(id) {
    let el = document.getElementById(id);
    if (el == undefined || el == null)
      return 'faq__category-m-heading';
    else {
      if (this.isInViewport(el)) {
        return 'faq__category-m-heading w--current';
      }
      else
        return 'faq__category-m-heading';
    }
}

**scrolltoElement 函数根据参数传递的 id 滚动到类别(因此现在它在视口中)。
**isInViewport 函数返回 true 如果类别当前在视口中(我第一次使用这个我不知道它是否可以增强)。
**getClass 根据类是否在视口中动态更改类。

代码“显然”有效,但会引发 expressionChangedAfterItHasBeenCheckedError。
附带问题:函数 getClass() 被每个对象调用了大约 3 次......为什么会发生这种情况?

预先感谢您的任何帮助,我尝试搜索类似的案例,但找不到任何解决方案。

解决方法

您看到 expressionChangedAfterItHasBeenCheckedError 错误是因为在开发模式下 DOM 已在 Angular 生命周期挂钩之外更新。

具体来说,当点击处理程序被调用时,这会告诉 Angular 检查视图的变化。单击处理程序返回后,视图会检查是否存在任何差异。但是,在您的点击处理程序中,您调用 scrollIntoView 函数,该函数本质上是异步的,因为您不知道元素何时会滚动到视图中。 getClass 在每个变更检测周期都会被调用。但是,您的 getClass() 方法依赖于 scrollIntoView 异步方法来突出显示视图中的任何 DOM。

Angular 尝试在开发模式下保持智能,并会在发生任何名义更改检测(即单击事件)后强制进行更改检测,以检查 DOM 中是否存在任何分歧以及 Angular 在其本地状态中保存的内容。这是为了防止在生产中发生未定义的行为。生产环境不会进行这种额外的更改检测,因此您的程序可能无法在生产环境中运行。

TL,博士。您看到 expressionChangedAfterItHasBeenCheckedError 是因为您依赖异步代码来更新 DOM。

解决方案:

  1. 将异步代码包装在 NgZone.run() 中。一旦异步代码完成,这将触发更新。
scrollToElement($elementId): void {
   this.currentID = $elementId;  //I was trying another method here
   let el = document.getElementById($elementId);
   this.ngZone.run(() =>
       el.scrollIntoView({ behavior: "smooth",block: "start",inline: "nearest" });
   ); 
  1. 不要依赖异步代码来更新 DOM。您应该能够通过在单击时在本地设置 elementId 来同步执行此操作。
// template.html
<a *ngFor="let category of DISPLAYED_CATEGORIES
    (click)="scrollToElement(category.FAQ_CATEGORY_ID)"
    [ngClass]="{'active': category.id === currentID}">
        {{category.FAQ_CATEGORY_TITLE}}
</a>