Angular - 动态组件呈现错误数据

Angular - Dynamic Component Rendering Wrong Data

已解决

正如 Aaron 在下面提到的,“其他选项”方法没有问题,而且感觉更干净。我将保留更新和所有内容,以防将来有人遇到类似问题并且解决方法以某种方式提供有用的东西。


我在尝试使用分页和排序在 Angular material table 中呈现动态组件时遇到一个奇怪的问题。

这是一个 Stackblitz 重现的问题。根据我的研究,这似乎是某种变化检测问题,尽管我对此可能是错误的,因为我不知道为什么会这样。如果有人对如何解决此问题有任何想法,或者如果我做错了什么,非常感谢反馈和帮助。

当前行为:

我在 material 设计 table 中使用 @ViewChildrenComponentFactoryResolver 来动态渲染组件有几页数据。一切看起来都很好,但是当使用 matSort 对数据进行排序时,一些具有动态组件的行显示错误的数据值,而该行的其余部分 (non-dynamic) 具有正确的数据值。请参阅下面的屏幕截图:

排序前:

排序后:

组件实例的控制台日志(注意传递了正确的状态字段,但呈现了错误的字段)。

预期行为:

排序时数据和呈现的组件准确显示。应该匹配 dataSource.data 字段。

问题的最小重现说明:

访问 Stackblitz 和 运行 应用程序。单击“订单状态”的排序 header。请注意 table 中的第一个条目是如何表示“已交付”,而实际数据源将其表示为“已取消”。也不是说该行的其余部分是准确的 - 只是动态呈现的组件不正确。

我尝试过的:

我看过的可能的相关帖子,但没有提到解决方案

这似乎是同一个问题,但从未得到答复。我希望提供 Stackblitz 将有助于获得相同的答案。根据 poster.

,这似乎是某种变化检测问题

When loading same component with different data, old data still showing - Angular 5

尝试显式调用 componentRefs 的 detectChanges() 和 markForCheck() chanceDetectionRef 方法,parent 组件,动态创建的实际组件,运气不好。

Angular dynamic component loader change detection issue

可能的解决方法/更奇怪的行为:

如果您按订单状态排序并像以前一样将错误的值放在第一行,然后分页到下一页并返回,组件会显示正确的值。我假设分页会强制在 material table 或模板中进行某种更改检测,或者类似的内容,但我不知道为什么排序不会那样做。我目前要做的工作是分页到下一页然后返回,但这绝不是一个 acceptable 解决方案,我真的很想了解我在这里做错了什么,或者这是什么修复方法。

更新 1

根据下面 Aaron 的其他见解,我添加了一个 Updated Stackblitz 和一个更简化的示例。仔细观察,分页似乎并没有在奇怪的行为中发挥作用。我可以仅使用 2 table 个条目和一个排序(删除所有分页代码)来重现该问题。我更改了传入的数据以匹配各行,因此可以更清楚地了解哪些行来自何处​​。

这是使用 2、3 和 4 记录排序前后的合并屏幕截图:

我目前正在尝试查看是否可以将 table 强制为 re-render(假设它可能会改变情况),并最终挖掘 [= 的源代码149=].

更新 2

我想出的一个解决方法是,在我使用 onSortChange() 中的 splice() 保存数据源副本后,就在进行过滤之前,将其设置为空数组逻辑,然后将其设置回过滤后的数组。我假设这会触发 material datasource/table 中的某种更新,从而强制执行 re-render 或更新。我会继续尝试找出是否有更好的方法来解决这个问题,因为这样做似乎很奇怪,并且在我最初的应用程序中我 h有一个 filterPredicate 当 filtering/resetting 过滤器时也会导致同样的问题。

更新 3

Aaaaa 最后,在对此进行更多研究之后,我至少找到了一个有意义的解决方案。按照我在更新 #2 中所说的内容,我发现了这个 post:

https://github.com/angular/components/issues/15972#issuecomment-490235603

这解释了为什么 table 没有获得更新的数据源。就我而言,由于我正在做 sort/filter,因此可能更难弄清楚它,并且认为它不会直接与此相关。在接下来的几天里,我仍然会探索更多的想法,但如果都不起作用,我会继续并在之后将其标记为已解决。

我不是 100% 确定发生了什么,但稍微研究了一下,我认为问题不是变化检测。我首先注释掉

之后的所有内容

cellTemplateRef.clear();createDynamicComponents()dynamic-table.components.ts 文件中。

这样做,肯定会看到该列中的所有内容都消失了。

然后采取另一种方法,只注释掉 cellTemplateRef.clear(),您会看到新组件被推到旧组件之上。

下一步是在添加新组件后删除旧组件...

在 Object.keys.. 循环之后:

    if (cellTemplateRef.length > 1) {
      cellTemplateRef.remove(1);
    } 

不知道为什么 clear() 没有完成这项工作,但删除清除并添加直接删除组件实例似乎是一种解决方法。

注意到这个解决方案甚至没有正确排序,下面是对正在发生的事情的一些更深入的了解。在您的 StatusIconComponent 上添加一个 @Input() 索引并将其显示在您的组件中。然后在你的循环中...

cellComponentRef.instance["index"] = i;

排序后您会在排序前看到类似这样的内容:

然后排序后...

请注意,行未按您期望的顺序呈现。

有了这个,我怀疑您甚至不必一直 add/remove 组件。他们可能不会重新呈现整行,也许只是移动它。

这仍然没有回答您的问题,但希望能提供更多见解。

其他选项

我怀疑数据单元格中 ng-template 上的 ViewChildren 的顺序不是您希望的那样。因此,与其在 table 中创建模板,不如创建一个 DynamicCellComponent 来执行所有组件工厂魔法。

所以您的 table 模板看起来像这样...

<td mat-cell *matCellDef="let data">
  <!-- moving your ngIfs to the component -->

  <app-dynamic-cell [columnDef]="column" [data]="data"></app-dynamic-cell>
</td>

然后创建一个类似于此的组件(基本上将大部分代码从 table 移过来)。

@Component({
  selector: "app-dynamic-cell",
  templateUrl: "./dynamic-cell.component.html",
  styleUrls: ["./dynamic-cell.component.css"]
})
export class DynamicCellComponent implements AfterViewInit {
  @Input() columnDef: DynamicTableColumn;
  @Input() data: any;

  @ViewChild("container", { read: ViewContainerRef })
  private container: ViewContainerRef;

  constructor(private componentFactoryResolver: ComponentFactoryResolver, private cdr: ChangeDetectorRef) {}

  isValueDynamicComponent(value: any) {
    return !(typeof value === "function");
  }

  ngAfterViewInit() {
    console.log(this.columnDef, this.data);
    const viewContainerRef = this.container;
    if (viewContainerRef) {
      viewContainerRef.clear();

      const { type: componentType, inputs: inputsFn } = (this.columnDef
        .value as any) as DynamicComponent;

      const componentInputs = inputsFn(this.data);

      // create the component instance and store a reference to it
      const componentFactory = this.componentFactoryResolver.resolveComponentFactory(
        componentType
      );

      const cellComponentRef = viewContainerRef.createComponent<
        typeof componentType
      >(componentFactory);

      // populate any properties of the component
      Object.keys(componentInputs).forEach(key => {
        cellComponentRef.instance[key] = componentInputs[key];
      });

      this.cdr.detectChanges();
    }
  }
}

然后组件 html 执行您之前在单元格定义中执行的操作。

<ng-container *ngIf="!isValueDynamicComponent(columnDef.value)">{{ columnDef.value(data)}}</ng-container>
<ng-container *ngIf="isValueDynamicComponent(columnDef.value)"#container></ng-container>

这样一来,您就不必关心数据的内部运作 table,以及呈现事物的顺序等