Angular - 动态组件渲染错误数据

Posted

技术标签:

【中文标题】Angular - 动态组件渲染错误数据【英文标题】:Angular - Dynamic Component Rendering Wrong Data 【发布时间】:2021-06-14 10:50:42 【问题描述】:

已解决

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


我在尝试使用分页和排序在 Angular 材质表中渲染动态组件时遇到了一个奇怪的问题。

这里是一个Stackblitz 与转载问题。根据我的研究,这似乎是某种变化检测问题,尽管我可能错了,因为我不知道为什么会发生这种情况。如果有人对如何解决此问题有任何想法,或者如果我做错了什么,我们将非常感谢您提供反馈和帮助。

当前行为:

我在 Material Design 表中使用 @ViewChildrenComponentFactoryResolver 来使用几页数据动态渲染组件。一切看起来都很好,但是当使用 matSort 对数据进行排序时,一些具有动态组件的行显示错误的数据值,而其余的行(非动态)具有正确的数据值。请看下面的截图:

排序前:

排序后:

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

预期行为:

当我排序时,数据和渲染的组件会准确显示。应该匹配 dataSource.data 字段。

用说明最小化重现问题:

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

我的尝试:

尝试使用 get() 和 set() 而不是 @Input() 设置状态并注册 ngOnChage,我在其中从组件本身调用 changeDetectorRef,但问题仍然存在。

尝试更改 ChangeDetectionStrategy - 没有人做任何事情。

也尝试了以下部分中的解决方案

在没有提到解决方案的情况下我看过的可能的相关帖子

这似乎是同一个问题,但从未得到答复。我希望提供 Stackblitz 将有助于获得相同的答案。根据发帖者的说法,这似乎是某种变化检测问题。

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

尝试显式调用 componentRefs 的 detectChanges() 和 markForCheck() chanceDetectionRef 方法,父组件是动态创建但没有运气的实际组件。

Angular dynamic component loader change detection issue

Change detection not working when creating a component via ComponentFactoryResolver

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

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

更新 1

根据 Aaron 在下面的其他见解,我添加了一个 Updated Stackblitz 以及一个更加简化的示例。仔细观察,分页似乎并没有成为奇怪行为的一个因素。我只需 2 个表格条目和一个排序(所有分页代码都已删除)即可重现该问题。我已经更改了传入的数据以匹配各行,以便更清楚哪些行来自哪里。

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

我目前正在尝试查看是否可以强制重新渲染表格(假设它可能会改变事物),并最终会挖掘表格的源代码。

更新 2

我能想出的一个解决方法是在执行过滤逻辑之前使用onSortChange() 中的splice() 保存它的副本后将我的dataSource 设置为一个空数组,然后将其设置回来到过滤后的数组。我假设这会触发材质数据源/表中的某种更新,从而强制重新渲染或更新。我会继续尝试找出是否有更好的方法,因为这样做似乎很奇怪,而且在我的原始应用程序中,我有一个 filterPredicate,它在过滤/重置过滤器时也会导致同样的问题。

更新 3

啊啊啊终于在对此进行了更多研究之后,我至少找到了一个有意义的解决方案。在我在更新 #2 上说完之后,我发现了这篇文章:

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

这解释了为什么表没有获得更新的数据源。就我而言,由于我正在进行排序/过滤,因此可能很难弄清楚这一点,并且认为它不会直接与此相关。在接下来的几天里,我仍会探索更多的想法,但如果两者都不起作用,我会在此之后将其标记为已解决。

【问题讨论】:

【参考方案1】:

我不是 100% 确定发生了什么,但稍微尝试一下,我认为问题不在于更改检测。我首先在

之后注释掉所有内容

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

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

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

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

在 Object.keys.... 循环之后:

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

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

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

cellComponentRef.instance["index"] = i;

排序后你会在排序前看到如下内容:

然后在排序之后...

请注意,这些行未按您预期的顺序呈现。

这样,我怀疑您甚至不必一直添加/删除组件。他们可能不会重新渲染整行,可能只是移动它。

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

其他选项

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

所以你的表格模板看起来像这样......

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

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

然后创建一个与此类似的组件(基本上将大部分代码从表中移出)。

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

这样,您就不必关心数据表的内部运作,以及呈现的顺序等。

【讨论】:

嗯,我明白你在说什么。 remove(1) 肯定会改变前几个动态组件,但有些仍然关闭(例如 Shipped #11)实际上是 Delivered。我将进一步探索remove(1),看看我是否能让它正常运行 可能值得深入研究 mat-table 和/或 cdk 表源代码,以了解它们是如何渲染事物的。我曾经构建了一个自定义数据源,这对了解幕后发生的事情非常有帮助。 您对已发货 #11 的评论很有趣。请注意,#11 第一次没有呈现(在单击排序之前),因为它位于第二页。我的另一个想法是,您可能需要get 和现有组件(如果已经存在)而不是删除/创建组件,并且只需更新其输入,如果不存在则仅创建。 我在上面提供了一些关于渲染顺序的更多见解。 使用 DynamicCellComponent 添加了另一个选项。

以上是关于Angular - 动态组件渲染错误数据的主要内容,如果未能解决你的问题,请参考以下文章

2020 -02- 07组件与模板

如何在此文件夹中创建一个全新的 Angular 组件?为啥我使用 Angular CLI 获得此错误消息?

路由器插座的Angular 2延迟渲染

angular2“不是已知元素”错误:一切都已尝试

Angular 7 动态内容投影

Angular - 使用自定义指令加载多个组件返回未定义