首页 > 解决方案 > Angular:在渲染父组件后更改子组件数据

问题描述

我有两个组件的情况:父组件 (AComponent) 将由包含对象的数组组成的数据传递给其子组件 (BComponent),该子组件将此数据显示到表元素中。直到rows被定义,表格将显示加载指示。下面是我的情况的一个非常基本的娱乐。

import data from '../../services/data.json';

@Component({
  selector: 'app-a',
  templateUrl: `
    <app-b [columns]="columns" [rows]="rows"></app-b>
  `,
  styleUrls: ['./a.component.scss']
})
export class AComponent implements OnInit {

  columns: string[];
  rows: any[];

  constructor() { }

  ngOnInit(): void {
    this.columns = ['title', 'year', 'cast', 'genres'];
    this.fetchData().then((res: any[]) => this.rows = res);
  }

  fetchData = (): Promise<any[]> => {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve(data);
      }, 1000);
    });
  }
}
@Component({
  selector: 'app-b',
  templateUrl: `
    <table>
      <tr>
        <td *ngFor="let column of columns">{{column}}</td>
      </tr>
      <tr *ngIf="!rows">
        <td [colSpan]="columns.length">Loading...</td>
      </tr>
      <ng-container *ngIf="rows">
        <tr *ngFor="let row of rows">
          <td>{{row.title}}</td>
          <td>{{row.year}}</td>
          <td>{{row.cast}}</td>
          <td>{{row.genres}}</td>
        </tr>
      </ng-container>
    </table>
  `,
  styleUrls: ['./b.component.scss']
})
export class BComponent implements OnInit {

  @Input() columns: string[];
  @Input() rows: any[];

  constructor() { }

  ngOnInit(): void { }

如果数据是从 API 获取的,这种方法可以正常工作,因此是异步的。我试图通过该fetchData函数模拟一个延迟的 API 调用。测试数据来自我从https://raw.githubusercontent.com/prust/wikipedia-movie-data/master/movies.json获得的本地 .json 文件

但是我面临的问题是,一旦我将本地数据从 AComponent 传递到 BComponent,它就不会从 API 中获取,因此没有更多的异步操作。所以我们替换

ngOnInit(): void {
  this.columns = ['title', 'year', 'cast', 'genres'];
  this.fetchData().then((res: any[]) => this.rows = res);
}

ngOnInit(): void {
  this.columns = ['title', 'year', 'cast', 'genres'];
  this.rows = data;
}

由于需要将大量数据放入 DOM 中,渲染 BComponent 的视图需要几秒钟的时间,并且整个视图(AComponent + BComponent)只有在渲染完成后才会显示。因此,在此期间,用户看到的是空白屏幕。

我想要实现的是首先渲染和显示 BComponent 的列和加载指示,然后是行,一旦它们也完全渲染,就替换加载指示。所以与从 API 异步获取数据时发生的情况基本相同。

我尝试了几件事但没有成功:

将 BComponent 拆分为两个单独的组件:一个用于列,一个用于行,这不起作用,因为父组件 (AComponent) 仅在所有子组件完全呈现时才会显示。

我还通过 Angular 生命周期钩子尝试了很多东西。例如将数据分配给ngAfterViewInit钩子中的行:

ngAfterViewInit(): void {
  this.rows = data;
}

但这会消除ExpressionChangedAfterItHasBeenCheckedError错误。

这本质上是我想要实现的:在 AComponent 视图(以及它的子视图)初始化之后分配和渲染行。但在 Angular 中,初始化和实际渲染似乎是两件不同的事情。据我所知,视图的渲染只有在 Angular 完成所有生命周期钩子后才开始。一个钩子AfterViewRendered或类似的东西会很方便。所以我认为任何带有钩子的东西都不适合我的情况?

我还尝试将本地数据包装在 Observables 或 Promise 中以使其异步:

ngOnInit(): void {
  this.columns = ['title', 'year', 'cast', 'genres'];
  Promise.resolve().then(() => this.rows = data);
}

但这也行不通,除非在setTimeout. 将本地数据分配给行需要“跳出”视图初始化和渲染 fase 以获得我想要的结果,这就是setTimeout. 但我希望有一种不那么肮脏的方式来实现这一目标。

我对这个没有想法......所以非常感谢任何关于不同方法的帮助或想法。

对不起,如果我有错误或误解。

谢谢你的时间!

编辑:我在我的表上实现了分页,但表本身是一个相对较重的组件,这解释了我认为的长渲染时间。本例中过大的data.json文件只是为了模仿表格渲染时间过长。

标签: angulartypescript

解决方案


方法一: 实现分页机制,无需一次显示上千行!我们人类无法一次处理这么多的数据。

方法2: 如果您必须一次显示所有数据,则实现虚拟滚动,其想法是仅呈现可见行,而其他行在向下滚动时按需呈现。

我将在此答案中详细说明您如何使用第二种方法:

确保已安装 angular cdk,如果未安装

npm i @angular/cdk

然后将其添加到您的应用程序模块

import { ScrollingModule } from '@angular/cdk/scrolling';
@NgModule({
 ...
 imports: [ ScrollingModule, ...]
})
export class AppModule {}  

接下来像这样在您的模板中使用它

  <table>
      <tr>
        <td *ngFor="let column of columns">{{column}}</td>
      </tr>
      <tr *ngIf="!rows">
        <td [colSpan]="columns.length">Loading...</td>
      </tr>
      <cdk-virtual-scroll-viewport  *ngIf="rows" itemSize="50"> 
        <tr *cdkVirtualFor="let row of rows">
          <td>{{row.title}}</td>
          <td>{{row.year}}</td>
          <td>{{row.cast}}</td>
          <td>{{row.genres}}</td>
        </tr>
     </cdk-virtual-scroll-viewport>
    </table>
 

您现在应该使用虚拟滚动获得更好的性能


推荐阅读