mirror of
https://github.com/bitwarden/browser.git
synced 2024-12-21 16:18:28 +01:00
[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:
parent
801d9a870b
commit
9b474264e6
@ -74,7 +74,7 @@ export class TableDataSource<T> extends DataSource<T> {
|
||||
}
|
||||
}
|
||||
|
||||
connect(): Observable<readonly T[]> {
|
||||
connect(): Observable<T[]> {
|
||||
if (!this._renderChangesSubscription) {
|
||||
this.updateChangeSubscription();
|
||||
}
|
||||
|
20
libs/components/src/table/table-scroll.component.html
Normal file
20
libs/components/src/table/table-scroll.component.html
Normal 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>
|
92
libs/components/src/table/table-scroll.component.ts
Normal file
92
libs/components/src/table/table-scroll.component.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
@ -6,7 +6,7 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
<ng-container
|
||||
*ngTemplateOutlet="templateVariable.template; context: { $implicit: rows }"
|
||||
*ngTemplateOutlet="templateVariable.template; context: { $implicit: rows$ }"
|
||||
></ng-container>
|
||||
</tbody>
|
||||
</table>
|
||||
|
@ -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;
|
||||
|
||||
@ -50,7 +50,7 @@ export class TableComponent implements OnDestroy, AfterContentChecked {
|
||||
this._initialized = true;
|
||||
|
||||
const dataStream = this.dataSource.connect();
|
||||
this.rows = dataStream;
|
||||
this.rows$ = dataStream;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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 {}
|
||||
|
@ -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: `
|
||||
<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>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
@ -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>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user