Skip to content

Commit

Permalink
[CL-343] Create a new table component for virtual scrolling (#10113)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
Hinton authored Oct 22, 2024
1 parent 801d9a8 commit 9b47426
Show file tree
Hide file tree
Showing 8 changed files with 173 additions and 62 deletions.
2 changes: 1 addition & 1 deletion libs/components/src/table/table-data-source.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ export class TableDataSource<T> extends DataSource<T> {
}
}

connect(): Observable<readonly T[]> {
connect(): Observable<T[]> {
if (!this._renderChangesSubscription) {
this.updateChangeSubscription();
}
Expand Down
20 changes: 20 additions & 0 deletions libs/components/src/table/table-scroll.component.html
Original file line number Diff line number Diff line change
@@ -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>
92 changes: 92 additions & 0 deletions libs/components/src/table/table-scroll.component.ts
Original file line number Diff line number Diff line change
@@ -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();
}
}
}
2 changes: 1 addition & 1 deletion libs/components/src/table/table.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
</thead>
<tbody>
<ng-container
*ngTemplateOutlet="templateVariable.template; context: { $implicit: rows }"
*ngTemplateOutlet="templateVariable.template; context: { $implicit: rows$ }"
></ng-container>
</tbody>
</table>
4 changes: 2 additions & 2 deletions libs/components/src/table/table.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export class TableComponent implements OnDestroy, AfterContentChecked {

@ContentChild(TableBodyDirective) templateVariable: TableBodyDirective;

protected rows: Observable<readonly any[]>;
protected rows$: Observable<any[]>;

private _initialized = false;

Expand All @@ -50,7 +50,7 @@ export class TableComponent implements OnDestroy, AfterContentChecked {
this._initialized = true;

const dataStream = this.dataSource.connect();
this.rows = dataStream;
this.rows$ = dataStream;
}
}

Expand Down
40 changes: 20 additions & 20 deletions libs/components/src/table/table.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -141,28 +141,28 @@ dataSource.filter = "search value";
### Virtual Scrolling

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,
specify a `itemSize`, set `scrollWindow` to `true` and replace `*ngFor` with `*cdkVirtualFor`.
of data. This is done by using the `bit-table-scroll` component instead of the `bit-table`
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
<cdk-virtual-scroll-viewport scrollWindow itemSize="47">
<bit-table [dataSource]="dataSource">
<ng-container header>
<tr>
<th bitCell bitSortable="id" default>Id</th>
<th bitCell bitSortable="name">Name</th>
<th bitCell bitSortable="other" [fn]="sortFn">Other</th>
</tr>
</ng-container>
<ng-template let-rows$>
<tr bitRow *cdkVirtualFor="let r of rows$">
<td bitCell>{{ r.id }}</td>
<td bitCell>{{ r.name }}</td>
<td bitCell>{{ r.other }}</td>
</tr>
</ng-template>
</bit-table>
</cdk-virtual-scroll-viewport>
<bit-table-scroll [dataSource]="dataSource" rowSize="47">
<ng-container header>
<th bitCell bitSortable="id" default>Id</th>
<th bitCell bitSortable="name">Name</th>
<th bitCell bitSortable="other" [fn]="sortFn">Other</th>
</ng-container>
<ng-template bitRowDef let-row>
<td bitCell>{{ row.id }}</td>
<td bitCell>{{ row.name }}</td>
<td bitCell>{{ row.other }}</td>
</ng-template>
</bit-table-scroll>
```

## Accessibility
Expand Down
15 changes: 13 additions & 2 deletions libs/components/src/table/table.module.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,31 @@
import { ScrollingModule } from "@angular/cdk/scrolling";
import { CommonModule } from "@angular/common";
import { NgModule } from "@angular/core";

import { CellDirective } from "./cell.directive";
import { RowDirective } from "./row.directive";
import { SortableComponent } from "./sortable.component";
import { BitRowDef, TableScrollComponent } from "./table-scroll.component";
import { TableBodyDirective, TableComponent } from "./table.component";

@NgModule({
imports: [CommonModule],
imports: [CommonModule, ScrollingModule, BitRowDef],
declarations: [
CellDirective,
RowDirective,
SortableComponent,
TableBodyDirective,
TableComponent,
TableScrollComponent,
],
exports: [
BitRowDef,
CellDirective,
RowDirective,
SortableComponent,
TableBodyDirective,
TableComponent,
TableScrollComponent,
],
exports: [TableComponent, CellDirective, RowDirective, SortableComponent, TableBodyDirective],
})
export class TableModule {}
60 changes: 24 additions & 36 deletions libs/components/src/table/table.stories.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { ScrollingModule } from "@angular/cdk/scrolling";
import { Meta, moduleMetadata, StoryObj } from "@storybook/angular";

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

0 comments on commit 9b47426

Please sign in to comment.