[CL-343] Create a new table component for virtual scrolling (#10113)

This creates a new component called bit-table-scroll as it's a breaking change in how tables works. We could probably conditionally support both behaviors in the existing table component if we desire.

Rather than iterating the rows in the consuming component, we now need to define a row definition, bitRowDef which provides access to the rows data through angular let- syntax. This allows the table component to own the behaviour which is needed in order to use the cdkVirtualFor directive which must be inside the cdk-virtual-scroll-viewport component.
This commit is contained in:
Oscar Hinton 2024-10-22 21:51:45 +02:00 committed by GitHub
parent 801d9a870b
commit 9b474264e6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 174 additions and 63 deletions

View File

@ -74,7 +74,7 @@ export class TableDataSource<T> extends DataSource<T> {
} }
} }
connect(): Observable<readonly T[]> { connect(): Observable<T[]> {
if (!this._renderChangesSubscription) { if (!this._renderChangesSubscription) {
this.updateChangeSubscription(); this.updateChangeSubscription();
} }

View File

@ -0,0 +1,20 @@
<cdk-virtual-scroll-viewport
scrollWindow
[itemSize]="rowSize"
[ngStyle]="{ paddingBottom: headerHeight + 'px' }"
>
<table [ngClass]="tableClass">
<thead
class="tw-border-0 tw-border-b-2 tw-border-solid tw-border-secondary-300 tw-font-bold tw-text-muted"
>
<tr>
<ng-content select="[header]"></ng-content>
</tr>
</thead>
<tbody>
<tr *cdkVirtualFor="let r of rows$; trackBy: trackBy" bitRow>
<ng-container *ngTemplateOutlet="rowDef.template; context: { $implicit: r }"></ng-container>
</tr>
</tbody>
</table>
</cdk-virtual-scroll-viewport>

View File

@ -0,0 +1,92 @@
import {
AfterContentChecked,
Component,
ContentChild,
Input,
OnDestroy,
TemplateRef,
Directive,
NgZone,
AfterViewInit,
ElementRef,
TrackByFunction,
} from "@angular/core";
import { TableComponent } from "./table.component";
/**
* Helper directive for defining the row template.
*
* ```html
* <ng-template bitRowDef let-row>
* <td bitCell>{{ row.id }}</td>
* </ng-template>
* ```
*/
@Directive({
selector: "[bitRowDef]",
standalone: true,
})
export class BitRowDef {
constructor(public template: TemplateRef<any>) {}
}
/**
* Scrollable table component.
*
* Utilizes virtual scrolling to render large datasets.
*/
@Component({
selector: "bit-table-scroll",
templateUrl: "./table-scroll.component.html",
providers: [{ provide: TableComponent, useExisting: TableScrollComponent }],
})
export class TableScrollComponent
extends TableComponent
implements AfterContentChecked, AfterViewInit, OnDestroy
{
/** The size of the rows in the list (in pixels). */
@Input({ required: true }) rowSize: number;
/** Optional trackBy function. */
@Input() trackBy: TrackByFunction<any> | undefined;
@ContentChild(BitRowDef) protected rowDef: BitRowDef;
/**
* Height of the thead element (in pixels).
*
* Used to increase the table's total height to avoid items being cut off.
*/
protected headerHeight = 0;
/**
* Observer for table header, applies padding on resize.
*/
private headerObserver: ResizeObserver;
constructor(
private zone: NgZone,
private el: ElementRef,
) {
super();
}
ngAfterViewInit(): void {
this.headerObserver = new ResizeObserver((entries) => {
this.zone.run(() => {
this.headerHeight = entries[0].contentRect.height;
});
});
this.headerObserver.observe(this.el.nativeElement.querySelector("thead"));
}
override ngOnDestroy(): void {
super.ngOnDestroy();
if (this.headerObserver) {
this.headerObserver.disconnect();
}
}
}

View File

@ -6,7 +6,7 @@
</thead> </thead>
<tbody> <tbody>
<ng-container <ng-container
*ngTemplateOutlet="templateVariable.template; context: { $implicit: rows }" *ngTemplateOutlet="templateVariable.template; context: { $implicit: rows$ }"
></ng-container> ></ng-container>
</tbody> </tbody>
</table> </table>

View File

@ -30,7 +30,7 @@ export class TableComponent implements OnDestroy, AfterContentChecked {
@ContentChild(TableBodyDirective) templateVariable: TableBodyDirective; @ContentChild(TableBodyDirective) templateVariable: TableBodyDirective;
protected rows: Observable<readonly any[]>; protected rows$: Observable<any[]>;
private _initialized = false; private _initialized = false;
@ -50,7 +50,7 @@ export class TableComponent implements OnDestroy, AfterContentChecked {
this._initialized = true; this._initialized = true;
const dataStream = this.dataSource.connect(); const dataStream = this.dataSource.connect();
this.rows = dataStream; this.rows$ = dataStream;
} }
} }

View File

@ -141,28 +141,28 @@ dataSource.filter = "search value";
### Virtual Scrolling ### Virtual Scrolling
It's heavily adviced to use virtual scrolling if you expect the table to have any significant amount It's heavily adviced to use virtual scrolling if you expect the table to have any significant amount
of data. This is easily done by wrapping the table in the `cdk-virtual-scroll-viewport` component, of data. This is done by using the `bit-table-scroll` component instead of the `bit-table`
specify a `itemSize`, set `scrollWindow` to `true` and replace `*ngFor` with `*cdkVirtualFor`. component. This component behaves slightly different from the `bit-table` component. Instead of
using the `*ngFor` directive to render the rows, you provide a `bitRowDef` template that will be
used for rendering the rows.
Due to limitations in the Angular Component Dev Kit you must provide an `rowSize` which corresponds
to the height of each row. If the height of the rows are not uniform, you should set an explicit row
height and align vertically.
```html ```html
<cdk-virtual-scroll-viewport scrollWindow itemSize="47"> <bit-table-scroll [dataSource]="dataSource" rowSize="47">
<bit-table [dataSource]="dataSource">
<ng-container header> <ng-container header>
<tr>
<th bitCell bitSortable="id" default>Id</th> <th bitCell bitSortable="id" default>Id</th>
<th bitCell bitSortable="name">Name</th> <th bitCell bitSortable="name">Name</th>
<th bitCell bitSortable="other" [fn]="sortFn">Other</th> <th bitCell bitSortable="other" [fn]="sortFn">Other</th>
</tr>
</ng-container> </ng-container>
<ng-template let-rows$> <ng-template bitRowDef let-row>
<tr bitRow *cdkVirtualFor="let r of rows$"> <td bitCell>{{ row.id }}</td>
<td bitCell>{{ r.id }}</td> <td bitCell>{{ row.name }}</td>
<td bitCell>{{ r.name }}</td> <td bitCell>{{ row.other }}</td>
<td bitCell>{{ r.other }}</td>
</tr>
</ng-template> </ng-template>
</bit-table> </bit-table-scroll>
</cdk-virtual-scroll-viewport>
``` ```
## Accessibility ## Accessibility

View File

@ -1,20 +1,31 @@
import { ScrollingModule } from "@angular/cdk/scrolling";
import { CommonModule } from "@angular/common"; import { CommonModule } from "@angular/common";
import { NgModule } from "@angular/core"; import { NgModule } from "@angular/core";
import { CellDirective } from "./cell.directive"; import { CellDirective } from "./cell.directive";
import { RowDirective } from "./row.directive"; import { RowDirective } from "./row.directive";
import { SortableComponent } from "./sortable.component"; import { SortableComponent } from "./sortable.component";
import { BitRowDef, TableScrollComponent } from "./table-scroll.component";
import { TableBodyDirective, TableComponent } from "./table.component"; import { TableBodyDirective, TableComponent } from "./table.component";
@NgModule({ @NgModule({
imports: [CommonModule], imports: [CommonModule, ScrollingModule, BitRowDef],
declarations: [ declarations: [
TableComponent,
CellDirective, CellDirective,
RowDirective, RowDirective,
SortableComponent, SortableComponent,
TableBodyDirective, TableBodyDirective,
TableComponent,
TableScrollComponent,
],
exports: [
BitRowDef,
CellDirective,
RowDirective,
SortableComponent,
TableBodyDirective,
TableComponent,
TableScrollComponent,
], ],
exports: [TableComponent, CellDirective, RowDirective, SortableComponent, TableBodyDirective],
}) })
export class TableModule {} export class TableModule {}

View File

@ -1,4 +1,3 @@
import { ScrollingModule } from "@angular/cdk/scrolling";
import { Meta, moduleMetadata, StoryObj } from "@storybook/angular"; import { Meta, moduleMetadata, StoryObj } from "@storybook/angular";
import { countries } from "../form/countries"; import { countries } from "../form/countries";
@ -10,7 +9,7 @@ export default {
title: "Component Library/Table", title: "Component Library/Table",
decorators: [ decorators: [
moduleMetadata({ moduleMetadata({
imports: [TableModule, ScrollingModule], imports: [TableModule],
}), }),
], ],
argTypes: { argTypes: {
@ -114,26 +113,21 @@ export const Scrollable: Story = {
props: { props: {
dataSource: data2, dataSource: data2,
sortFn: (a: any, b: any) => a.id - b.id, sortFn: (a: any, b: any) => a.id - b.id,
trackBy: (index: number, item: any) => item.id,
}, },
template: ` template: `
<cdk-virtual-scroll-viewport scrollWindow itemSize="47"> <bit-table-scroll [dataSource]="dataSource" [rowSize]="43">
<bit-table [dataSource]="dataSource">
<ng-container header> <ng-container header>
<tr>
<th bitCell bitSortable="id" default>Id</th> <th bitCell bitSortable="id" default>Id</th>
<th bitCell bitSortable="name">Name</th> <th bitCell bitSortable="name">Name</th>
<th bitCell bitSortable="other" [fn]="sortFn">Other</th> <th bitCell bitSortable="other" [fn]="sortFn">Other</th>
</tr>
</ng-container> </ng-container>
<ng-template body let-rows$> <ng-template bitRowDef let-row>
<tr bitRow *cdkVirtualFor="let r of rows$"> <td bitCell>{{ row.id }}</td>
<td bitCell>{{ r.id }}</td> <td bitCell>{{ row.name }}</td>
<td bitCell>{{ r.name }}</td> <td bitCell>{{ row.other }}</td>
<td bitCell>{{ r.other }}</td>
</tr>
</ng-template> </ng-template>
</bit-table> </bit-table-scroll>
</cdk-virtual-scroll-viewport>
`, `,
}), }),
}; };
@ -151,22 +145,16 @@ export const Filterable: Story = {
}, },
template: ` template: `
<input type="search" placeholder="Search" (input)="dataSource.filter = $event.target.value" /> <input type="search" placeholder="Search" (input)="dataSource.filter = $event.target.value" />
<cdk-virtual-scroll-viewport scrollWindow itemSize="47"> <bit-table-scroll [dataSource]="dataSource" [rowSize]="43">
<bit-table [dataSource]="dataSource">
<ng-container header> <ng-container header>
<tr>
<th bitCell bitSortable="name" default>Name</th> <th bitCell bitSortable="name" default>Name</th>
<th bitCell bitSortable="value" width="120px">Value</th> <th bitCell bitSortable="value" width="120px">Value</th>
</tr>
</ng-container> </ng-container>
<ng-template body let-rows$> <ng-template bitRowDef let-row>
<tr bitRow *cdkVirtualFor="let r of rows$"> <td bitCell>{{ row.name }}</td>
<td bitCell>{{ r.name }}</td> <td bitCell>{{ row.value }}</td>
<td bitCell>{{ r.value }}</td>
</tr>
</ng-template> </ng-template>
</bit-table> </bit-table-scroll>
</cdk-virtual-scroll-viewport>
`, `,
}), }),
}; };