mirror of
https://github.com/bitwarden/browser.git
synced 2024-11-25 12:15:18 +01:00
[SM-74] TableDataSource for sorting (#4079)
* Initial draft of a table data source * Improve table data source * Migrate projects table for demo * Update existing tables * Fix access selector * remove sortDirection from custom fn * a11y improvements * update icons; make button full width * update storybook docs * apply code review changes * fix: add table body to projects list * Fix error on create secret. Fix project list setting projects on getter. Copy table data on set. Fix documentation * Change signature to protected, rename method to not start with underscore * add hover and focus effects Co-authored-by: William Martin <contact@willmartian.com>
This commit is contained in:
parent
23897ae5fb
commit
344a054ba2
@ -40,7 +40,7 @@
|
||||
<th bitCell style="width: 50px"></th>
|
||||
</tr>
|
||||
</ng-container>
|
||||
<ng-container body formArrayName="items">
|
||||
<ng-template body formArrayName="items">
|
||||
<tr
|
||||
bitRow
|
||||
*ngFor="let item of selectionList.selectedItems; let i = index"
|
||||
@ -134,5 +134,5 @@
|
||||
<tr *ngIf="selectionList.selectedItems.length == 0">
|
||||
<td bitCell>{{ emptySelectionText }}</td>
|
||||
</tr>
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
</bit-table>
|
||||
|
@ -75,7 +75,7 @@
|
||||
<th bitCell>{{ "event" | i18n }}</th>
|
||||
</tr>
|
||||
</ng-container>
|
||||
<ng-container body>
|
||||
<ng-template body>
|
||||
<tr bitRow *ngFor="let e of events" alignContent="top">
|
||||
<td bitCell class="tw-whitespace-nowrap">{{ e.date | date: "medium" }}</td>
|
||||
<td bitCell>
|
||||
@ -86,7 +86,7 @@
|
||||
</td>
|
||||
<td bitCell [innerHTML]="e.message"></td>
|
||||
</tr>
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
</bit-table>
|
||||
<button
|
||||
#moreBtn
|
||||
|
@ -16,12 +16,12 @@
|
||||
<th bitCell>{{ "error" | i18n }}</th>
|
||||
</tr>
|
||||
</ng-container>
|
||||
<ng-container body>
|
||||
<ng-template body>
|
||||
<tr bitRow *ngFor="let detail of data.details">
|
||||
<td bitCell>{{ detail.name }}</td>
|
||||
<td bitCell>{{ detail.errorMessage }}</td>
|
||||
</tr>
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
</bit-table>
|
||||
</div>
|
||||
|
||||
|
@ -11,7 +11,7 @@
|
||||
</button>
|
||||
</sm-no-items>
|
||||
|
||||
<bit-table *ngIf="projects?.length >= 1">
|
||||
<bit-table *ngIf="projects?.length >= 1" [dataSource]="dataSource">
|
||||
<ng-container header>
|
||||
<tr>
|
||||
<th bitCell class="tw-w-0">
|
||||
@ -25,8 +25,8 @@
|
||||
{{ "all" | i18n }}
|
||||
</label>
|
||||
</th>
|
||||
<th bitCell colspan="2">{{ "name" | i18n }}</th>
|
||||
<th bitCell>{{ "lastEdited" | i18n }}</th>
|
||||
<th bitCell colspan="2" bitSortable="name" default>{{ "name" | i18n }}</th>
|
||||
<th bitCell bitSortable="revisionDate">{{ "lastEdited" | i18n }}</th>
|
||||
<th bitCell class="tw-w-0">
|
||||
<button
|
||||
bitIconButton="bwi-ellipsis-v"
|
||||
@ -38,8 +38,8 @@
|
||||
</th>
|
||||
</tr>
|
||||
</ng-container>
|
||||
<ng-container body>
|
||||
<tr bitRow *ngFor="let project of projects; index as i">
|
||||
<ng-template body let-rows$>
|
||||
<tr bitRow *ngFor="let project of rows$ | async">
|
||||
<td bitCell>
|
||||
<input
|
||||
type="checkbox"
|
||||
@ -78,7 +78,7 @@
|
||||
</button>
|
||||
</bit-menu>
|
||||
</tr>
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
</bit-table>
|
||||
|
||||
<bit-menu #tableMenu>
|
||||
|
@ -2,6 +2,8 @@ import { SelectionModel } from "@angular/cdk/collections";
|
||||
import { Component, EventEmitter, Input, OnDestroy, Output } from "@angular/core";
|
||||
import { Subject, takeUntil } from "rxjs";
|
||||
|
||||
import { TableDataSource } from "@bitwarden/components";
|
||||
|
||||
import { ProjectListView } from "../../models/view/project-list.view";
|
||||
|
||||
@Component({
|
||||
@ -9,6 +11,8 @@ import { ProjectListView } from "../../models/view/project-list.view";
|
||||
templateUrl: "./projects-list.component.html",
|
||||
})
|
||||
export class ProjectsListComponent implements OnDestroy {
|
||||
protected dataSource = new TableDataSource<ProjectListView>();
|
||||
|
||||
@Input()
|
||||
get projects(): ProjectListView[] {
|
||||
return this._projects;
|
||||
@ -16,6 +20,7 @@ export class ProjectsListComponent implements OnDestroy {
|
||||
set projects(projects: ProjectListView[]) {
|
||||
this.selection.clear();
|
||||
this._projects = projects;
|
||||
this.dataSource.data = projects;
|
||||
}
|
||||
private _projects: ProjectListView[];
|
||||
|
||||
|
@ -44,7 +44,7 @@
|
||||
<th bitCell></th>
|
||||
</tr>
|
||||
</ng-container>
|
||||
<ng-container body *ngIf="selectedProjects != null">
|
||||
<ng-template body *ngIf="selectedProjects != null">
|
||||
<tr bitRow *ngFor="let e of selectedProjects">
|
||||
<td bitCell>{{ e.name }}</td>
|
||||
<td bitCell class="tw-w-0">
|
||||
@ -57,7 +57,7 @@
|
||||
></button>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
</bit-table>
|
||||
</bit-tab>
|
||||
</bit-tab-group>
|
||||
|
@ -39,7 +39,7 @@
|
||||
</th>
|
||||
</tr>
|
||||
</ng-container>
|
||||
<ng-container body>
|
||||
<ng-template body>
|
||||
<tr bitRow *ngFor="let token of tokens">
|
||||
<td bitCell>
|
||||
<input
|
||||
@ -73,5 +73,5 @@
|
||||
</button>
|
||||
</bit-menu>
|
||||
</tr>
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
</bit-table>
|
||||
|
@ -29,13 +29,13 @@
|
||||
<th bitCell>{{ "permissions" | i18n }}</th>
|
||||
</tr>
|
||||
</ng-container>
|
||||
<ng-container body>
|
||||
<ng-template body>
|
||||
<tr>
|
||||
<!-- TODO once access is implement display selected access -->
|
||||
<td bitCell>example</td>
|
||||
<td bitCell>example</td>
|
||||
</tr>
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
</bit-table>
|
||||
</div>
|
||||
<div bitDialogFooter class="tw-flex tw-gap-2">
|
||||
|
@ -39,7 +39,7 @@
|
||||
</th>
|
||||
</tr>
|
||||
</ng-container>
|
||||
<ng-container body>
|
||||
<ng-template body>
|
||||
<tr bitRow *ngFor="let serviceAccount of serviceAccounts">
|
||||
<td bitCell>
|
||||
<input
|
||||
@ -83,7 +83,7 @@
|
||||
</button>
|
||||
</bit-menu>
|
||||
</tr>
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
</bit-table>
|
||||
|
||||
<bit-menu #tableMenu>
|
||||
|
@ -39,7 +39,7 @@
|
||||
</th>
|
||||
</tr>
|
||||
</ng-container>
|
||||
<ng-container body>
|
||||
<ng-template body>
|
||||
<tr bitRow *ngFor="let secret of secrets">
|
||||
<td bitCell>
|
||||
<input
|
||||
@ -96,7 +96,7 @@
|
||||
</button>
|
||||
</bit-menu>
|
||||
</tr>
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
</bit-table>
|
||||
|
||||
<bit-menu #tableMenu>
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Meta, Story, Source } from "@storybook/addon-docs";
|
||||
import { Meta, Story } from "@storybook/addon-docs";
|
||||
|
||||
<Meta title="Documentation/Button" />
|
||||
|
||||
|
104
libs/components/src/stories/table-docs.stories.mdx
Normal file
104
libs/components/src/stories/table-docs.stories.mdx
Normal file
@ -0,0 +1,104 @@
|
||||
import { Meta, Story, Source } from "@storybook/addon-docs";
|
||||
|
||||
<Meta title="Documentation/Table" />
|
||||
|
||||
# Table
|
||||
|
||||
## Overview
|
||||
|
||||
All tables should have a visible horizontal header and label for each column.
|
||||
|
||||
<Story id="component-library-table--default" />
|
||||
|
||||
The below code is the absolute minimum required to create a table. However we stronly advice you to
|
||||
use the `dataSource` input to provide a data source for your table. This allows you to easily sort
|
||||
data.
|
||||
|
||||
```html
|
||||
<bit-table>
|
||||
<ng-container header>
|
||||
<tr>
|
||||
<th bitCell>Header 1</th>
|
||||
<th bitCell>Header 2</th>
|
||||
<th bitCell>Header 3</th>
|
||||
</tr>
|
||||
</ng-container>
|
||||
<ng-template body>
|
||||
<tr bitRow>
|
||||
<td bitCell>Cell 1</td>
|
||||
<td bitCell>Cell 2</td>
|
||||
<td bitCell>Cell 3</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</bit-table>
|
||||
```
|
||||
|
||||
## Data Source
|
||||
|
||||
Bitwarden provides a data source for tables that can be used in place of a traditional data array.
|
||||
The `TableDataSource` implements sorting and will in the future also support filtering. This allows
|
||||
the `bitTable` component to focus on rendering while offloading the data management to the data
|
||||
source.
|
||||
|
||||
```ts
|
||||
// External data source
|
||||
const data: T[];
|
||||
|
||||
const dataSource = new TableDataSource<T>();
|
||||
dataSource.data = data;
|
||||
```
|
||||
|
||||
We use the `dataSource` as an input to the `bit-table` component, and access the rows to render
|
||||
within the `ng-template`which provides access to the rows using `let-rows$`.
|
||||
|
||||
<Source id="component-library-table--data-source" />
|
||||
|
||||
### Sortable
|
||||
|
||||
We provide a simple component for displaying sortable column headers. The `bitSortable` component
|
||||
wires up to the `TableDataSource` and will automatically sort the data when clicked and display
|
||||
an indicator for which column is currently sorted. The dafault sorting can be specified by setting
|
||||
the `default`.
|
||||
|
||||
```html
|
||||
<th bitCell bitSortable="id" default>Id</th>
|
||||
<th bitCell bitSortable="name" default>Name</th>
|
||||
```
|
||||
|
||||
It's also possible to define a custom sorting function by setting the `fn` input.
|
||||
|
||||
```ts
|
||||
const sortFn = (a: T, b: T) => (a.id > b.id ? 1 : -1);
|
||||
```
|
||||
|
||||
### 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`.
|
||||
|
||||
```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>
|
||||
```
|
||||
|
||||
## Accessibility
|
||||
|
||||
- Always incude a row or column header with your table; this allows screen readers to better contextualize the data
|
||||
- Avoid spanning data across cells
|
@ -1 +1,2 @@
|
||||
export * from "./table.module";
|
||||
export * from "./table-data-source";
|
||||
|
125
libs/components/src/table/sortable.component.ts
Normal file
125
libs/components/src/table/sortable.component.ts
Normal file
@ -0,0 +1,125 @@
|
||||
import { coerceBooleanProperty } from "@angular/cdk/coercion";
|
||||
import { Component, HostBinding, Input, OnInit } from "@angular/core";
|
||||
|
||||
import type { SortFn } from "./table-data-source";
|
||||
import { TableComponent } from "./table.component";
|
||||
|
||||
@Component({
|
||||
selector: "th[bitSortable]",
|
||||
template: `
|
||||
<button
|
||||
class="tw-group"
|
||||
[ngClass]="classList"
|
||||
[attr.aria-pressed]="isActive"
|
||||
(click)="setActive()"
|
||||
>
|
||||
<ng-content></ng-content>
|
||||
<i class="bwi tw-ml-2" [ngClass]="icon"></i>
|
||||
</button>
|
||||
`,
|
||||
})
|
||||
export class SortableComponent implements OnInit {
|
||||
/**
|
||||
* Mark the column as sortable and specify the key to sort by
|
||||
*/
|
||||
@Input() bitSortable: string;
|
||||
|
||||
private _default: boolean;
|
||||
/**
|
||||
* Mark the column as the default sort column
|
||||
*/
|
||||
@Input() set default(value: boolean | "") {
|
||||
this._default = coerceBooleanProperty(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom sorting function
|
||||
*
|
||||
* @example
|
||||
* fn = (a, b) => a.name.localeCompare(b.name)
|
||||
*/
|
||||
@Input() fn: SortFn;
|
||||
|
||||
constructor(private table: TableComponent) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
if (this._default && !this.isActive) {
|
||||
this.setActive();
|
||||
}
|
||||
}
|
||||
|
||||
@HostBinding("attr.aria-sort") get ariaSort() {
|
||||
if (!this.isActive) {
|
||||
return undefined;
|
||||
}
|
||||
return this.sort.direction === "asc" ? "ascending" : "descending";
|
||||
}
|
||||
|
||||
protected setActive() {
|
||||
if (this.table.dataSource) {
|
||||
const direction = this.isActive && this.direction === "asc" ? "desc" : "asc";
|
||||
this.table.dataSource.sort = { column: this.bitSortable, direction: direction, fn: this.fn };
|
||||
}
|
||||
}
|
||||
|
||||
private get sort() {
|
||||
return this.table.dataSource?.sort;
|
||||
}
|
||||
|
||||
get isActive() {
|
||||
return this.sort?.column === this.bitSortable;
|
||||
}
|
||||
|
||||
get direction() {
|
||||
return this.sort?.direction;
|
||||
}
|
||||
|
||||
get icon() {
|
||||
if (!this.isActive) {
|
||||
return "bwi-chevron-up tw-opacity-0 group-hover:tw-opacity-100 group-focus-visible:tw-opacity-100";
|
||||
}
|
||||
return this.direction === "asc" ? "bwi-chevron-up" : "bwi-angle-down";
|
||||
}
|
||||
|
||||
get classList() {
|
||||
return [
|
||||
// Offset to border and padding
|
||||
"-tw-m-1.5",
|
||||
|
||||
// Below is copied from BitIconButtonComponent
|
||||
"tw-font-semibold",
|
||||
"tw-border",
|
||||
"tw-border-solid",
|
||||
"tw-rounded",
|
||||
"tw-transition",
|
||||
"hover:tw-no-underline",
|
||||
"focus:tw-outline-none",
|
||||
|
||||
"tw-bg-transparent",
|
||||
"!tw-text-muted",
|
||||
"tw-border-transparent",
|
||||
"hover:tw-bg-transparent-hover",
|
||||
"hover:tw-border-primary-700",
|
||||
"focus-visible:before:tw-ring-primary-700",
|
||||
"disabled:tw-opacity-60",
|
||||
"disabled:hover:tw-border-transparent",
|
||||
"disabled:hover:tw-bg-transparent",
|
||||
|
||||
// Workaround for box-shadow with transparent offset issue:
|
||||
// https://github.com/tailwindlabs/tailwindcss/issues/3595
|
||||
// Remove `before:` and use regular `tw-ring` when browser no longer has bug, or better:
|
||||
// switch to `outline` with `outline-offset` when Safari supports border radius on outline.
|
||||
// Using `box-shadow` to create outlines is a hack and as such `outline` should be preferred.
|
||||
"tw-relative",
|
||||
"before:tw-content-['']",
|
||||
"before:tw-block",
|
||||
"before:tw-absolute",
|
||||
"before:-tw-inset-[3px]",
|
||||
"before:tw-rounded-md",
|
||||
"before:tw-transition",
|
||||
"before:tw-ring",
|
||||
"before:tw-ring-transparent",
|
||||
"focus-visible:tw-z-10",
|
||||
];
|
||||
}
|
||||
}
|
164
libs/components/src/table/table-data-source.ts
Normal file
164
libs/components/src/table/table-data-source.ts
Normal file
@ -0,0 +1,164 @@
|
||||
import { _isNumberValue } from "@angular/cdk/coercion";
|
||||
import { DataSource } from "@angular/cdk/collections";
|
||||
import { BehaviorSubject, combineLatest, map, Observable, Subscription } from "rxjs";
|
||||
|
||||
export type SortDirection = "asc" | "desc";
|
||||
export type SortFn = (a: any, b: any) => number;
|
||||
export type Sort = {
|
||||
column?: string;
|
||||
direction: SortDirection;
|
||||
fn?: SortFn;
|
||||
};
|
||||
|
||||
// Loosely based on CDK TableDataSource
|
||||
// https://github.com/angular/components/blob/main/src/material/table/table-data-source.ts
|
||||
export class TableDataSource<T> extends DataSource<T> {
|
||||
private readonly _data: BehaviorSubject<T[]>;
|
||||
private readonly _sort: BehaviorSubject<Sort>;
|
||||
private readonly _renderData = new BehaviorSubject<T[]>([]);
|
||||
|
||||
private _renderChangesSubscription: Subscription | null = null;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this._data = new BehaviorSubject([]);
|
||||
this._sort = new BehaviorSubject({ direction: "asc" });
|
||||
}
|
||||
|
||||
get data() {
|
||||
return this._data.value;
|
||||
}
|
||||
|
||||
set data(data: T[]) {
|
||||
this._data.next(data ? [...data] : []);
|
||||
}
|
||||
|
||||
set sort(sort: Sort) {
|
||||
this._sort.next(sort);
|
||||
}
|
||||
|
||||
get sort() {
|
||||
return this._sort.value;
|
||||
}
|
||||
|
||||
connect(): Observable<readonly T[]> {
|
||||
if (!this._renderChangesSubscription) {
|
||||
this.updateChangeSubscription();
|
||||
}
|
||||
|
||||
return this._renderData;
|
||||
}
|
||||
|
||||
disconnect(): void {
|
||||
this._renderChangesSubscription?.unsubscribe();
|
||||
this._renderChangesSubscription = null;
|
||||
}
|
||||
|
||||
private updateChangeSubscription() {
|
||||
const orderedData = combineLatest([this._data, this._sort]).pipe(
|
||||
map(([data]) => this.orderData(data))
|
||||
);
|
||||
|
||||
this._renderChangesSubscription?.unsubscribe();
|
||||
this._renderChangesSubscription = orderedData.subscribe((data) => this._renderData.next(data));
|
||||
}
|
||||
|
||||
private orderData(data: T[]): T[] {
|
||||
if (!this.sort) {
|
||||
return data;
|
||||
}
|
||||
|
||||
return this.sortData(data, this.sort);
|
||||
}
|
||||
|
||||
/**
|
||||
* Copied from https://github.com/angular/components/blob/main/src/material/table/table-data-source.ts
|
||||
* License: MIT
|
||||
* Copyright (c) 2022 Google LLC.
|
||||
*
|
||||
* Data accessor function that is used for accessing data properties for sorting through
|
||||
* the default sortData function.
|
||||
* This default function assumes that the sort header IDs (which defaults to the column name)
|
||||
* matches the data's properties (e.g. column Xyz represents data['Xyz']).
|
||||
* May be set to a custom function for different behavior.
|
||||
* @param data Data object that is being accessed.
|
||||
* @param sortHeaderId The name of the column that represents the data.
|
||||
*/
|
||||
protected sortingDataAccessor(data: T, sortHeaderId: string): string | number {
|
||||
const value = (data as unknown as Record<string, any>)[sortHeaderId];
|
||||
|
||||
if (_isNumberValue(value)) {
|
||||
const numberValue = Number(value);
|
||||
|
||||
return numberValue < Number.MAX_SAFE_INTEGER ? numberValue : value;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Copied from https://github.com/angular/components/blob/main/src/material/table/table-data-source.ts
|
||||
* License: MIT
|
||||
* Copyright (c) 2022 Google LLC.
|
||||
*
|
||||
* Gets a sorted copy of the data array based on the state of the MatSort. Called
|
||||
* after changes are made to the filtered data or when sort changes are emitted from MatSort.
|
||||
* By default, the function retrieves the active sort and its direction and compares data
|
||||
* by retrieving data using the sortingDataAccessor. May be overridden for a custom implementation
|
||||
* of data ordering.
|
||||
* @param data The array of data that should be sorted.
|
||||
* @param sort The connected MatSort that holds the current sort state.
|
||||
*/
|
||||
protected sortData(data: T[], sort: Sort): T[] {
|
||||
const column = sort.column;
|
||||
const direction = sort.direction;
|
||||
if (!column) {
|
||||
return data;
|
||||
}
|
||||
|
||||
return data.sort((a, b) => {
|
||||
// If a custom sort function is provided, use it instead of the default.
|
||||
if (sort.fn) {
|
||||
return sort.fn(a, b) * (direction === "asc" ? 1 : -1);
|
||||
}
|
||||
|
||||
let valueA = this.sortingDataAccessor(a, column);
|
||||
let valueB = this.sortingDataAccessor(b, column);
|
||||
|
||||
// If there are data in the column that can be converted to a number,
|
||||
// it must be ensured that the rest of the data
|
||||
// is of the same type so as not to order incorrectly.
|
||||
const valueAType = typeof valueA;
|
||||
const valueBType = typeof valueB;
|
||||
|
||||
if (valueAType !== valueBType) {
|
||||
if (valueAType === "number") {
|
||||
valueA += "";
|
||||
}
|
||||
if (valueBType === "number") {
|
||||
valueB += "";
|
||||
}
|
||||
}
|
||||
|
||||
// If both valueA and valueB exist (truthy), then compare the two. Otherwise, check if
|
||||
// one value exists while the other doesn't. In this case, existing value should come last.
|
||||
// This avoids inconsistent results when comparing values to undefined/null.
|
||||
// If neither value exists, return 0 (equal).
|
||||
let comparatorResult = 0;
|
||||
if (valueA != null && valueB != null) {
|
||||
// Check if one value is greater than the other; if equal, comparatorResult should remain 0.
|
||||
if (valueA > valueB) {
|
||||
comparatorResult = 1;
|
||||
} else if (valueA < valueB) {
|
||||
comparatorResult = -1;
|
||||
}
|
||||
} else if (valueA != null) {
|
||||
comparatorResult = 1;
|
||||
} else if (valueB != null) {
|
||||
comparatorResult = -1;
|
||||
}
|
||||
|
||||
return comparatorResult * (direction === "asc" ? 1 : -1);
|
||||
});
|
||||
}
|
||||
}
|
@ -5,6 +5,8 @@
|
||||
<ng-content select="[header]"></ng-content>
|
||||
</thead>
|
||||
<tbody>
|
||||
<ng-content select="[body]"></ng-content>
|
||||
<ng-container
|
||||
*ngTemplateOutlet="templateVariable.template; context: { $implicit: rows }"
|
||||
></ng-container>
|
||||
</tbody>
|
||||
</table>
|
||||
|
@ -1,7 +1,50 @@
|
||||
import { Component } from "@angular/core";
|
||||
import { isDataSource } from "@angular/cdk/collections";
|
||||
import {
|
||||
AfterContentChecked,
|
||||
Component,
|
||||
ContentChild,
|
||||
Directive,
|
||||
Input,
|
||||
OnDestroy,
|
||||
TemplateRef,
|
||||
} from "@angular/core";
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
import { TableDataSource } from "./table-data-source";
|
||||
|
||||
@Directive({
|
||||
selector: "ng-template[body]",
|
||||
})
|
||||
export class TableBodyDirective {
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-member-accessibility
|
||||
constructor(public readonly template: TemplateRef<any>) {}
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: "bit-table",
|
||||
templateUrl: "./table.component.html",
|
||||
})
|
||||
export class TableComponent {}
|
||||
export class TableComponent implements OnDestroy, AfterContentChecked {
|
||||
@Input() dataSource: TableDataSource<any>;
|
||||
|
||||
@ContentChild(TableBodyDirective) templateVariable: TableBodyDirective;
|
||||
|
||||
protected rows: Observable<readonly any[]>;
|
||||
|
||||
private _initialized = false;
|
||||
|
||||
ngAfterContentChecked(): void {
|
||||
if (!this._initialized && isDataSource(this.dataSource)) {
|
||||
this._initialized = true;
|
||||
|
||||
const dataStream = this.dataSource.connect();
|
||||
this.rows = dataStream;
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
if (isDataSource(this.dataSource)) {
|
||||
this.dataSource.disconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,11 +3,18 @@ import { NgModule } from "@angular/core";
|
||||
|
||||
import { CellDirective } from "./cell.directive";
|
||||
import { RowDirective } from "./row.directive";
|
||||
import { TableComponent } from "./table.component";
|
||||
import { SortableComponent } from "./sortable.component";
|
||||
import { TableBodyDirective, TableComponent } from "./table.component";
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule],
|
||||
declarations: [TableComponent, CellDirective, RowDirective],
|
||||
exports: [TableComponent, CellDirective, RowDirective],
|
||||
declarations: [
|
||||
TableComponent,
|
||||
CellDirective,
|
||||
RowDirective,
|
||||
SortableComponent,
|
||||
TableBodyDirective,
|
||||
],
|
||||
exports: [TableComponent, CellDirective, RowDirective, SortableComponent, TableBodyDirective],
|
||||
})
|
||||
export class TableModule {}
|
||||
|
@ -1,12 +1,14 @@
|
||||
import { ScrollingModule } from "@angular/cdk/scrolling";
|
||||
import { Meta, moduleMetadata, Story } from "@storybook/angular";
|
||||
|
||||
import { TableDataSource } from "./table-data-source";
|
||||
import { TableModule } from "./table.module";
|
||||
|
||||
export default {
|
||||
title: "Component Library/Table",
|
||||
decorators: [
|
||||
moduleMetadata({
|
||||
imports: [TableModule],
|
||||
imports: [TableModule, ScrollingModule],
|
||||
}),
|
||||
],
|
||||
argTypes: {
|
||||
@ -34,7 +36,7 @@ const Template: Story = (args) => ({
|
||||
<th bitCell>Header 3</th>
|
||||
</tr>
|
||||
</ng-container>
|
||||
<ng-container body>
|
||||
<ng-template body>
|
||||
<tr bitRow [alignContent]="alignRowContent">
|
||||
<td bitCell>Cell 1</td>
|
||||
<td bitCell>Cell 2 <br> Multiline Cell</td>
|
||||
@ -50,9 +52,8 @@ const Template: Story = (args) => ({
|
||||
<td bitCell>Cell 8</td>
|
||||
<td bitCell>Cell 9</td>
|
||||
</tr>
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
</bit-table>
|
||||
|
||||
`,
|
||||
});
|
||||
|
||||
@ -60,3 +61,75 @@ export const Default = Template.bind({});
|
||||
Default.args = {
|
||||
alignRowContent: "baseline",
|
||||
};
|
||||
|
||||
const data = new TableDataSource<{ id: number; name: string; other: string }>();
|
||||
|
||||
data.data = [...Array(5).keys()].map((i) => ({
|
||||
id: i,
|
||||
name: `name-${i}`,
|
||||
other: `other-${i}`,
|
||||
}));
|
||||
|
||||
const DataSourceTemplate: Story = (args) => ({
|
||||
props: {
|
||||
dataSource: data,
|
||||
sortFn: (a: any, b: any) => a.id - b.id,
|
||||
},
|
||||
template: `
|
||||
<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 *ngFor="let r of rows$ | async">
|
||||
<td bitCell>{{ r.id }}</td>
|
||||
<td bitCell>{{ r.name }}</td>
|
||||
<td bitCell>{{ r.other }}</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</bit-table>
|
||||
`,
|
||||
});
|
||||
|
||||
export const DataSource = DataSourceTemplate.bind({});
|
||||
|
||||
const data2 = new TableDataSource<{ id: number; name: string; other: string }>();
|
||||
|
||||
data2.data = [...Array(100).keys()].map((i) => ({
|
||||
id: i,
|
||||
name: `name-${i}`,
|
||||
other: `other-${i}`,
|
||||
}));
|
||||
|
||||
const ScrollableTemplate: Story = (args) => ({
|
||||
props: {
|
||||
dataSource: data2,
|
||||
sortFn: (a: any, b: any) => a.id - b.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>
|
||||
`,
|
||||
});
|
||||
|
||||
export const Scrollable = ScrollableTemplate.bind({});
|
||||
|
Loading…
Reference in New Issue
Block a user