From 9b474264e6ca9d7fd1071b2a27d0ad647838dd30 Mon Sep 17 00:00:00 2001 From: Oscar Hinton Date: Tue, 22 Oct 2024 21:51:45 +0200 Subject: [PATCH] [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. --- .../components/src/table/table-data-source.ts | 2 +- .../src/table/table-scroll.component.html | 20 ++++ .../src/table/table-scroll.component.ts | 92 +++++++++++++++++++ .../components/src/table/table.component.html | 2 +- libs/components/src/table/table.component.ts | 4 +- libs/components/src/table/table.mdx | 40 ++++---- libs/components/src/table/table.module.ts | 17 +++- libs/components/src/table/table.stories.ts | 60 +++++------- 8 files changed, 174 insertions(+), 63 deletions(-) create mode 100644 libs/components/src/table/table-scroll.component.html create mode 100644 libs/components/src/table/table-scroll.component.ts diff --git a/libs/components/src/table/table-data-source.ts b/libs/components/src/table/table-data-source.ts index 36f5866763..6501c9bffb 100644 --- a/libs/components/src/table/table-data-source.ts +++ b/libs/components/src/table/table-data-source.ts @@ -74,7 +74,7 @@ export class TableDataSource extends DataSource { } } - connect(): Observable { + connect(): Observable { if (!this._renderChangesSubscription) { this.updateChangeSubscription(); } diff --git a/libs/components/src/table/table-scroll.component.html b/libs/components/src/table/table-scroll.component.html new file mode 100644 index 0000000000..26b06ee0e5 --- /dev/null +++ b/libs/components/src/table/table-scroll.component.html @@ -0,0 +1,20 @@ + + + + + + + + + + + + +
+
diff --git a/libs/components/src/table/table-scroll.component.ts b/libs/components/src/table/table-scroll.component.ts new file mode 100644 index 0000000000..77647133bc --- /dev/null +++ b/libs/components/src/table/table-scroll.component.ts @@ -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 + * + * {{ row.id }} + * + * ``` + */ +@Directive({ + selector: "[bitRowDef]", + standalone: true, +}) +export class BitRowDef { + constructor(public template: TemplateRef) {} +} + +/** + * 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 | 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(); + } + } +} diff --git a/libs/components/src/table/table.component.html b/libs/components/src/table/table.component.html index 09b035826c..6615d0cb42 100644 --- a/libs/components/src/table/table.component.html +++ b/libs/components/src/table/table.component.html @@ -6,7 +6,7 @@ diff --git a/libs/components/src/table/table.component.ts b/libs/components/src/table/table.component.ts index b4d6d1931d..16f7a01b7c 100644 --- a/libs/components/src/table/table.component.ts +++ b/libs/components/src/table/table.component.ts @@ -30,7 +30,7 @@ export class TableComponent implements OnDestroy, AfterContentChecked { @ContentChild(TableBodyDirective) templateVariable: TableBodyDirective; - protected rows: Observable; + protected rows$: Observable; private _initialized = false; @@ -50,7 +50,7 @@ export class TableComponent implements OnDestroy, AfterContentChecked { this._initialized = true; const dataStream = this.dataSource.connect(); - this.rows = dataStream; + this.rows$ = dataStream; } } diff --git a/libs/components/src/table/table.mdx b/libs/components/src/table/table.mdx index f08691f764..3f28dd93b6 100644 --- a/libs/components/src/table/table.mdx +++ b/libs/components/src/table/table.mdx @@ -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 - - - - - Id - Name - Other - - - - - {{ r.id }} - {{ r.name }} - {{ r.other }} - - - - + + + Id + Name + Other + + + {{ row.id }} + {{ row.name }} + {{ row.other }} + + ``` ## Accessibility diff --git a/libs/components/src/table/table.module.ts b/libs/components/src/table/table.module.ts index 753e4362e6..1f1b705c69 100644 --- a/libs/components/src/table/table.module.ts +++ b/libs/components/src/table/table.module.ts @@ -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: [ - TableComponent, CellDirective, RowDirective, SortableComponent, TableBodyDirective, + TableComponent, + TableScrollComponent, + ], + exports: [ + BitRowDef, + CellDirective, + RowDirective, + SortableComponent, + TableBodyDirective, + TableComponent, + TableScrollComponent, ], - exports: [TableComponent, CellDirective, RowDirective, SortableComponent, TableBodyDirective], }) export class TableModule {} diff --git a/libs/components/src/table/table.stories.ts b/libs/components/src/table/table.stories.ts index 8fa3f5559d..4ebc3045d1 100644 --- a/libs/components/src/table/table.stories.ts +++ b/libs/components/src/table/table.stories.ts @@ -1,4 +1,3 @@ -import { ScrollingModule } from "@angular/cdk/scrolling"; import { Meta, moduleMetadata, StoryObj } from "@storybook/angular"; import { countries } from "../form/countries"; @@ -10,7 +9,7 @@ export default { title: "Component Library/Table", decorators: [ moduleMetadata({ - imports: [TableModule, ScrollingModule], + imports: [TableModule], }), ], argTypes: { @@ -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: ` - - - - - Id - Name - Other - - - - - {{ r.id }} - {{ r.name }} - {{ r.other }} - - - - + + + Id + Name + Other + + + {{ row.id }} + {{ row.name }} + {{ row.other }} + + `, }), }; @@ -151,22 +145,16 @@ export const Filterable: Story = { }, template: ` - - - - - Name - Value - - - - - {{ r.name }} - {{ r.value }} - - - - + + + Name + Value + + + {{ row.name }} + {{ row.value }} + + `, }), };