From b121ea9fe83aea0cc0106de78be54e214b1efabd Mon Sep 17 00:00:00 2001 From: tcoile Date: Tue, 28 Jan 2025 12:26:58 -0800 Subject: [PATCH 01/47] fix: refactor ideas --- .../table-example.component.html | 36 ++++++++++++++++++- .../table-example/table-example.component.ts | 31 +++++++++------- .../src/lib/table/table.component.html | 2 +- .../src/lib/table/table.component.ts | 20 +++++++++++ 4 files changed, 74 insertions(+), 15 deletions(-) diff --git a/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.html b/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.html index 0f9d1efb2..a7a9e468e 100644 --- a/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.html +++ b/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.html @@ -1 +1,35 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
No. {{ element.position }} Name {{ element.name }} + Weight + {{ element.weight }} Symbol {{ element.symbol }}
diff --git a/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.ts b/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.ts index 3518dac82..0697359c2 100644 --- a/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.ts +++ b/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.ts @@ -12,23 +12,28 @@ import { BehaviorSubject } from 'rxjs'; changeDetection: ChangeDetectionStrategy.OnPush, }) export class TableExampleComponent { - config$ = new BehaviorSubject({ + data$ = new BehaviorSubject({ data: [ { fruit: 'apple', color: 'red' }, { fruit: 'orange', color: 'orange' }, { fruit: 'banana', color: 'yellow' }, ], - columns: [ - new TableColumn<{ fruit: string; color: string }>({ - label: 'Fruit', - getFormattedValue: (x) => x.fruit, - sortable: true, - }), - new TableColumn<{ fruit: string; color: string }>({ - label: 'Color', - getFormattedValue: (x) => x.color, - sortable: true, - }), - ], }); + + sortColumnConfig = [ + new TableColumn<{ fruit: string; color: string }>({ + cdkColumnDef: 'fruit', + ascendingSortFunction: (a, b) => a.fruit.localeCompare(b.fruit), + sortOrder: 1, + sortDirection: 'asc', // initial sort direction + }), + new TableColumn<{ fruit: string; color: string }>({ + cdkColumnDef: 'color', + sortable: true, + ascendingSortFunction: (a, b) => a.color.localeCompare(b.color), + }), + ]; + dataSource = new HsiUiTableDataSource(data$, sortColumnConfig); + + // handleSort() } diff --git a/libs/ui-components/src/lib/table/table.component.html b/libs/ui-components/src/lib/table/table.component.html index b755c2b2f..fd73d2b9b 100644 --- a/libs/ui-components/src/lib/table/table.component.html +++ b/libs/ui-components/src/lib/table/table.component.html @@ -21,7 +21,7 @@ [ngClass]="[column.getAlignment(element), 'sorted-header']" [column]="column" [sortIcon]="sortIcon" - (click)="sortTableByColumn(column)" + (click)="handleSort(column)" > implements OnInit { return column.label; } } + + +hsiUiTableHeader +class HsiUiTableDataSource extends DataSource { + + // user inputs the full data + // user inputs some column sorting configuration + constructor(private inputData$: Observable, private sortConfig: TableColumn[]) { + super(); + this.transformedData$ = combineLatest([sortConfig$, this.inputData$]).pipe(return subsetData) + } + + handleSort(column: whateverDataTypeThisIs) { + this.sortConfig[column] -- what is the sort function? use some smart default if not provided + handle tiebreaks using this.sortConfig[column].sortOrder (will need other columns' sort functions & orders) + } + override connect(): Observable { + return this.transformedData$; + } +} \ No newline at end of file From 2187c63a3329afc5bcc6378c425c6113954a95d7 Mon Sep 17 00:00:00 2001 From: Ally Choung Date: Tue, 4 Feb 2025 15:23:47 -0500 Subject: [PATCH 02/47] ci: add data source file --- .../table-example/table-example.component.ts | 26 ++++++++------- .../documentation-directory.yaml | 1 + libs/ui-components/src/lib/table/index.ts | 1 + .../src/lib/table/table.component.html | 4 +-- .../src/lib/table/table.data-source.ts | 32 +++++++++++++++++++ 5 files changed, 50 insertions(+), 14 deletions(-) create mode 100644 libs/ui-components/src/lib/table/table.data-source.ts diff --git a/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.ts b/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.ts index 0697359c2..88f48dfc8 100644 --- a/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.ts +++ b/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.ts @@ -1,7 +1,11 @@ import { CommonModule } from '@angular/common'; import { ChangeDetectionStrategy, Component } from '@angular/core'; -import { TableColumn, TableModule } from '@hsi/ui-components'; -import { BehaviorSubject } from 'rxjs'; +import { + HsiUiTableDataSource, + TableColumn, + TableModule, +} from '@hsi/ui-components'; +import { of } from 'rxjs'; @Component({ selector: 'app-table-example', @@ -12,28 +16,26 @@ import { BehaviorSubject } from 'rxjs'; changeDetection: ChangeDetectionStrategy.OnPush, }) export class TableExampleComponent { - data$ = new BehaviorSubject({ - data: [ - { fruit: 'apple', color: 'red' }, - { fruit: 'orange', color: 'orange' }, - { fruit: 'banana', color: 'yellow' }, - ], - }); + data$ = of([ + { fruit: 'apple', color: 'red' }, + { fruit: 'orange', color: 'orange' }, + { fruit: 'banana', color: 'yellow' }, + ]); sortColumnConfig = [ new TableColumn<{ fruit: string; color: string }>({ - cdkColumnDef: 'fruit', + // cdkColumnDef: 'fruit', ascendingSortFunction: (a, b) => a.fruit.localeCompare(b.fruit), sortOrder: 1, sortDirection: 'asc', // initial sort direction }), new TableColumn<{ fruit: string; color: string }>({ - cdkColumnDef: 'color', + // cdkColumnDef: 'color', sortable: true, ascendingSortFunction: (a, b) => a.color.localeCompare(b.color), }), ]; - dataSource = new HsiUiTableDataSource(data$, sortColumnConfig); + dataSource = new HsiUiTableDataSource(this.data$, this.sortColumnConfig); // handleSort() } diff --git a/apps/demo-app/src/assets/ui-components/documentation/documentation-directory.yaml b/apps/demo-app/src/assets/ui-components/documentation/documentation-directory.yaml index cb5fb671a..ddc3eb26f 100644 --- a/apps/demo-app/src/assets/ui-components/documentation/documentation-directory.yaml +++ b/apps/demo-app/src/assets/ui-components/documentation/documentation-directory.yaml @@ -16,6 +16,7 @@ directory: table: table: 'components/TableComponent.html' table-config: 'classes/HsiUiTableConfig.html' + table-data-source: 'classes/HsiUiTableDataSource.html' table-column: 'classes/TableColumn.html' single-sort-header: 'components/SingleSortHeaderComponent.html' tabs: diff --git a/libs/ui-components/src/lib/table/index.ts b/libs/ui-components/src/lib/table/index.ts index 88817cae2..8959328b8 100644 --- a/libs/ui-components/src/lib/table/index.ts +++ b/libs/ui-components/src/lib/table/index.ts @@ -1,4 +1,5 @@ export * from './table-column'; export * from './table.component'; export * from './table.config'; +export * from './table.data-source'; export * from './table.module'; diff --git a/libs/ui-components/src/lib/table/table.component.html b/libs/ui-components/src/lib/table/table.component.html index fd73d2b9b..534bd53af 100644 --- a/libs/ui-components/src/lib/table/table.component.html +++ b/libs/ui-components/src/lib/table/table.component.html @@ -1,4 +1,4 @@ - +
+ +
extends ArrayDataSource { + private transformedData$: Observable; + // user inputs the full data + // user inputs some column sorting configuration + constructor( + private inputData$: Observable, + private sortConfig$: TableColumn[] + ) { + super(inputData$); + this.transformedData$ = combineLatest([sortConfig$, inputData$]).pipe( + map(([sort, data]) => data) + ); + } + + handleSort(column: TableColumn) { + // this.sortConfig[column] -- what is the sort function? use some smart default if not provided + // handle tiebreaks using this.sortConfig[column].sortOrder (will need other columns' sort functions & orders) + } + + override disconnect(): void {} + + override connect(): Observable { + return this.transformedData$; + } +} From 4b8ae18b84e0e237b590df19ea83872f8a7464d2 Mon Sep 17 00:00:00 2001 From: Ally Choung Date: Tue, 4 Feb 2025 17:21:09 -0500 Subject: [PATCH 03/47] fix: changes to table example html, co-authored by Tom Coile --- .../table-example.component.html | 59 ++++++++----------- .../table-example/table-example.component.ts | 19 +++--- .../single-sort-header.component.spec.ts | 4 +- .../src/lib/table/table-column.ts | 4 +- .../src/lib/table/table.module.ts | 2 +- 5 files changed, 43 insertions(+), 45 deletions(-) diff --git a/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.html b/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.html index a7a9e468e..cb431a307 100644 --- a/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.html +++ b/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.html @@ -1,35 +1,28 @@ - - - - - - - +@if (columns$ | async; as columns) { +
No. {{ element.position }}
+ + + + - - - - - + + + + - - - - - - - - - - - - - - -
+ Fruit + {{ element.fruit }} Name {{ element.name }} + Color + {{ element.color }} - Weight - {{ element.weight }} Symbol {{ element.symbol }}
+
+} diff --git a/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.ts b/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.ts index 88f48dfc8..8debbc8e1 100644 --- a/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.ts +++ b/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.ts @@ -7,6 +7,11 @@ import { } from '@hsi/ui-components'; import { of } from 'rxjs'; +enum ColumnNames { + fruit = 'fruit', + color = 'color', +} + @Component({ selector: 'app-table-example', standalone: true, @@ -16,26 +21,26 @@ import { of } from 'rxjs'; changeDetection: ChangeDetectionStrategy.OnPush, }) export class TableExampleComponent { + columnIds = [ColumnNames.fruit, ColumnNames.color]; data$ = of([ { fruit: 'apple', color: 'red' }, { fruit: 'orange', color: 'orange' }, { fruit: 'banana', color: 'yellow' }, ]); + ColumnNames = ColumnNames; - sortColumnConfig = [ + columns$ = of([ new TableColumn<{ fruit: string; color: string }>({ - // cdkColumnDef: 'fruit', + id: ColumnNames.fruit, ascendingSortFunction: (a, b) => a.fruit.localeCompare(b.fruit), sortOrder: 1, sortDirection: 'asc', // initial sort direction }), new TableColumn<{ fruit: string; color: string }>({ - // cdkColumnDef: 'color', + id: ColumnNames.color, sortable: true, ascendingSortFunction: (a, b) => a.color.localeCompare(b.color), }), - ]; - dataSource = new HsiUiTableDataSource(this.data$, this.sortColumnConfig); - - // handleSort() + ]); + dataSource = new HsiUiTableDataSource(this.data$, this.columns$); } diff --git a/libs/ui-components/src/lib/table/single-sort-header/single-sort-header.component.spec.ts b/libs/ui-components/src/lib/table/single-sort-header/single-sort-header.component.spec.ts index 593f888ec..41a312609 100644 --- a/libs/ui-components/src/lib/table/single-sort-header/single-sort-header.component.spec.ts +++ b/libs/ui-components/src/lib/table/single-sort-header/single-sort-header.component.spec.ts @@ -19,7 +19,7 @@ describe('SingleSortHeaderComponent', () => { column = new TableColumn<{ name: string }>({ getFormattedValue: (x) => x.name, sortDirection: SortDirection.asc, - label: 'name', + id: 'name', }); component.column = column; component.sortIcon = 'sortIcon'; @@ -28,7 +28,7 @@ describe('SingleSortHeaderComponent', () => { describe('getColumnSortClasses', () => { it('should return sort classes - case: is actively sorted', () => { component.column = new TableColumn<{ name: string }>({ - label: 'test', + id: 'test', getFormattedValue: (x) => x.name, sortDirection: SortDirection.asc, activelySorted: true, diff --git a/libs/ui-components/src/lib/table/table-column.ts b/libs/ui-components/src/lib/table/table-column.ts index 2edb4376b..aca280f4b 100644 --- a/libs/ui-components/src/lib/table/table-column.ts +++ b/libs/ui-components/src/lib/table/table-column.ts @@ -11,9 +11,9 @@ export type TableCellAlignment = 'left' | 'center' | 'right'; export class TableColumn { /** - * The label of the column. Used in the table header. + * The id of the column. Used in the table header. * */ - label: string; + id: string; /** * Function to extract the value to be sorted on from the datum. * If not provided, the formatted value will be used for sorting. diff --git a/libs/ui-components/src/lib/table/table.module.ts b/libs/ui-components/src/lib/table/table.module.ts index e4d256bc8..46cd03841 100644 --- a/libs/ui-components/src/lib/table/table.module.ts +++ b/libs/ui-components/src/lib/table/table.module.ts @@ -8,6 +8,6 @@ import { TableComponent } from './table.component'; @NgModule({ declarations: [TableComponent, SingleSortHeaderComponent], imports: [CommonModule, CdkTableModule, MatIconModule], - exports: [TableComponent], + exports: [TableComponent, CdkTableModule], }) export class TableModule {} From cd908b26bcd520191368dcfaf2ed279ae82a1c5c Mon Sep 17 00:00:00 2001 From: Ally Choung Date: Tue, 4 Feb 2025 17:33:59 -0500 Subject: [PATCH 04/47] ci: temp for data source --- .../src/lib/table/table.data-source.ts | 80 +++++++++++++++---- 1 file changed, 63 insertions(+), 17 deletions(-) diff --git a/libs/ui-components/src/lib/table/table.data-source.ts b/libs/ui-components/src/lib/table/table.data-source.ts index c12cf1cdc..58c4d29bc 100644 --- a/libs/ui-components/src/lib/table/table.data-source.ts +++ b/libs/ui-components/src/lib/table/table.data-source.ts @@ -1,32 +1,78 @@ -import { ArrayDataSource } from '@angular/cdk/collections'; +import { CollectionViewer, DataSource } from '@angular/cdk/collections'; import { CdkHeaderRowDef } from '@angular/cdk/table'; -import { Observable, combineLatest, map } from 'rxjs'; -import { TableColumn } from './table-column'; +import { BehaviorSubject, Observable, combineLatest, map } from 'rxjs'; +import { SortDirection, TableColumn } from './table-column'; export class HsiUiTableHeader extends CdkHeaderRowDef {} -export class HsiUiTableDataSource extends ArrayDataSource { - private transformedData$: Observable; +export class HsiUiTableDataSource extends DataSource { + private data = new BehaviorSubject([]); + private data$ = this.data.asObservable(); + private columns = new BehaviorSubject[]>([]); // user inputs the full data // user inputs some column sorting configuration + + // TODO: get rid of subscribe, use rxjs operators instead + // TODO: clean up table-column.ts only use properties used in this class + // TODO: add sort icon to table example + // TODO: plan sort column directive constructor( - private inputData$: Observable, - private sortConfig$: TableColumn[] + private inputData$: Observable, + private columns$: Observable[]> ) { - super(inputData$); - this.transformedData$ = combineLatest([sortConfig$, inputData$]).pipe( - map(([sort, data]) => data) - ); + super(); + + columns$.subscribe((columns) => { + this.columns.next(columns); + }); + combineLatest([columns$, inputData$]) + .pipe( + map(([sort, data]) => { + console.log(data, sort); + return data; + }) + ) + .subscribe((data) => this.data.next(data)); } - handleSort(column: TableColumn) { - // this.sortConfig[column] -- what is the sort function? use some smart default if not provided - // handle tiebreaks using this.sortConfig[column].sortOrder (will need other columns' sort functions & orders) + handleSort(columnId: string) { + const sortedColumns = this.columns + .getValue() + .slice() + .sort((columnA, columnB) => { + return columnA.id === columnId + ? -1 + : columnB.id === columnId + ? 1 + : columnA.sortOrder - columnB.sortOrder; + }); + + const sortedData = this.data + .getValue() + .slice() + .sort((a, b) => { + for (const column of sortedColumns) { + let returnValue = column.ascendingSortFunction(a, b); + if (column.sortDirection === SortDirection.desc) { + returnValue *= -1; + } + if (returnValue !== 0) return returnValue; + } + return 0; + }); + + sortedColumns[0].sortDirection = + sortedColumns[0].sortDirection == SortDirection.asc + ? SortDirection.desc + : SortDirection.asc; + + this.columns.next(sortedColumns); + this.data.next(sortedData); } - override disconnect(): void {} + override disconnect(_collectionViewer: CollectionViewer): void {} - override connect(): Observable { - return this.transformedData$; + override connect(_collectionViewer: CollectionViewer): Observable { + return this.data$; } } From 031168054365f75846e0fabb347c3d2d85ac556d Mon Sep 17 00:00:00 2001 From: Ally Choung Date: Wed, 5 Feb 2025 15:29:45 -0500 Subject: [PATCH 05/47] fix: use rxjs in data source for table --- .../single-sort-header.component.html | 2 +- .../src/lib/table/table.component.html | 6 +- .../src/lib/table/table.component.spec.ts | 2 +- .../src/lib/table/table.component.ts | 30 +-- .../src/lib/table/table.data-source.ts | 198 +++++++++++++----- 5 files changed, 157 insertions(+), 81 deletions(-) diff --git a/libs/ui-components/src/lib/table/single-sort-header/single-sort-header.component.html b/libs/ui-components/src/lib/table/single-sort-header/single-sort-header.component.html index aa0ba0fe1..75d987622 100644 --- a/libs/ui-components/src/lib/table/single-sort-header/single-sort-header.component.html +++ b/libs/ui-components/src/lib/table/single-sort-header/single-sort-header.component.html @@ -1,4 +1,4 @@
- {{ column.label }} + {{ column.id }} {{ sortIcon }}
diff --git a/libs/ui-components/src/lib/table/table.component.html b/libs/ui-components/src/lib/table/table.component.html index 534bd53af..bcde6f0fe 100644 --- a/libs/ui-components/src/lib/table/table.component.html +++ b/libs/ui-components/src/lib/table/table.component.html @@ -1,4 +1,4 @@ - + diff --git a/libs/ui-components/src/lib/table/table.component.spec.ts b/libs/ui-components/src/lib/table/table.component.spec.ts index ed257f72f..87ea26bb4 100644 --- a/libs/ui-components/src/lib/table/table.component.spec.ts +++ b/libs/ui-components/src/lib/table/table.component.spec.ts @@ -65,7 +65,7 @@ describe('TableComponent', () => { }>({ getSortValue: (x) => x.name, sortDirection: SortDirection.asc, - label: 'name', + id: 'name', sortOrder: 1, }); const columns = [ diff --git a/libs/ui-components/src/lib/table/table.component.ts b/libs/ui-components/src/lib/table/table.component.ts index bcbe02d86..b38e63fcb 100644 --- a/libs/ui-components/src/lib/table/table.component.ts +++ b/libs/ui-components/src/lib/table/table.component.ts @@ -107,7 +107,7 @@ export class TableComponent implements OnInit { toggleSortDirection = true ): TableColumn[] { const columnsWithSortDir = columns.map((x) => { - if (x.label === activeSortColumn.label) { + if (x.id === activeSortColumn.id) { if (toggleSortDirection) { x.sortDirection = x.sortDirection === SortDirection.asc @@ -128,7 +128,7 @@ export class TableComponent implements OnInit { setTableHeaders(): void { this.tableHeaders$ = this.columns$.pipe( - map((columns) => columns.map((x) => x.label)), + map((columns) => columns.map((x) => x.id)), distinctUntilChanged((a, b) => isEqual(a, b)), shareReplay(1) ); @@ -157,9 +157,9 @@ export class TableComponent implements OnInit { columns: TableColumn[] ): Datum[] { const sortedColumns = columns.slice().sort((columnA, columnB) => { - return columnA.label === primaryColumnSort.label + return columnA.id === primaryColumnSort.id ? -1 - : columnB.label === primaryColumnSort.label + : columnB.id === primaryColumnSort.id ? 1 : columnA.sortOrder - columnB.sortOrder; }); @@ -178,26 +178,6 @@ export class TableComponent implements OnInit { } columnTrackingFunction(_: number, column: TableColumn) { - return column.label; + return column.id; } } - - -hsiUiTableHeader -class HsiUiTableDataSource extends DataSource { - - // user inputs the full data - // user inputs some column sorting configuration - constructor(private inputData$: Observable, private sortConfig: TableColumn[]) { - super(); - this.transformedData$ = combineLatest([sortConfig$, this.inputData$]).pipe(return subsetData) - } - - handleSort(column: whateverDataTypeThisIs) { - this.sortConfig[column] -- what is the sort function? use some smart default if not provided - handle tiebreaks using this.sortConfig[column].sortOrder (will need other columns' sort functions & orders) - } - override connect(): Observable { - return this.transformedData$; - } -} \ No newline at end of file diff --git a/libs/ui-components/src/lib/table/table.data-source.ts b/libs/ui-components/src/lib/table/table.data-source.ts index 58c4d29bc..8f64c51c0 100644 --- a/libs/ui-components/src/lib/table/table.data-source.ts +++ b/libs/ui-components/src/lib/table/table.data-source.ts @@ -1,16 +1,22 @@ import { CollectionViewer, DataSource } from '@angular/cdk/collections'; -import { CdkHeaderRowDef } from '@angular/cdk/table'; -import { BehaviorSubject, Observable, combineLatest, map } from 'rxjs'; +import { + BehaviorSubject, + Observable, + combineLatest, + filter, + map, + merge, + scan, + shareReplay, + withLatestFrom, +} from 'rxjs'; import { SortDirection, TableColumn } from './table-column'; -export class HsiUiTableHeader extends CdkHeaderRowDef {} - export class HsiUiTableDataSource extends DataSource { - private data = new BehaviorSubject([]); - private data$ = this.data.asObservable(); - private columns = new BehaviorSubject[]>([]); - // user inputs the full data - // user inputs some column sorting configuration + private sortedData$: Observable; + + private sortColId = new BehaviorSubject(null); + private sortColId$ = this.sortColId.asObservable(); // TODO: get rid of subscribe, use rxjs operators instead // TODO: clean up table-column.ts only use properties used in this class @@ -18,61 +24,151 @@ export class HsiUiTableDataSource extends DataSource { // TODO: plan sort column directive constructor( private inputData$: Observable, - private columns$: Observable[]> + private inputColumns$: Observable[]> ) { super(); - columns$.subscribe((columns) => { - this.columns.next(columns); - }); - combineLatest([columns$, inputData$]) - .pipe( - map(([sort, data]) => { - console.log(data, sort); - return data; - }) + // this.columns$ = this.inputColumns$ + // .getValue() + // .slice() + // .sort((columnA, columnB) => { + // return columnA.id === columnId + // ? -1 + // : columnB.id === columnId + // ? 1 + // : columnA.sortOrder - columnB.sortOrder; + // }); + + // const sortedData = this.data + // .getValue() + // .slice() + // .sort((a, b) => { + // for (const column of sortedColumns) { + // let returnValue = column.ascendingSortFunction(a, b); + // if (column.sortDirection === SortDirection.desc) { + // returnValue *= -1; + // } + // if (returnValue !== 0) return returnValue; + // } + // return 0; + // }); + + // sortedColumns[0].sortDirection = + // sortedColumns[0].sortDirection == SortDirection.asc + // ? SortDirection.desc + // : SortDirection.asc; + + const config$ = combineLatest([this.inputData$, this.inputColumns$]).pipe( + withLatestFrom(this.sortColId$), + map(([[data, cols], sortId]) => () => { + // const activeSortColumn = + // cols.find((c) => c.id == sortId) || this.getMinSortOrderColumn(cols); + const columns = this.getColumnsWithNewSortApplied(sortId, cols, false); + return { + data: this.sortData(data, sortId, columns), + columns, + }; + }) + ); + + const sort$ = this.sortColId$.pipe( + filter((sortId) => sortId !== null), + map( + (sortId) => + (sortedConfig: { data: Datum[]; columns: TableColumn[] }) => { + const columns = this.getColumnsWithNewSortApplied( + sortId, + sortedConfig.columns + ); + return { + data: this.sortData(sortedConfig.data, sortId, columns), + columns: columns, + }; + } ) - .subscribe((data) => this.data.next(data)); + ); + + const sortedConfig$ = merge(config$, sort$).pipe( + scan((sortedConfig, changeFn) => changeFn(sortedConfig), { + data: [], + columns: [], + }), + shareReplay(1) // do not remove sort toggle will be called twice + ); + + this.sortedData$ = sortedConfig$.pipe( + map((x) => x.data), + shareReplay(1) + ); + this.inputColumns$ = sortedConfig$.pipe( + map((x) => x.columns), + shareReplay(1) + ); } - handleSort(columnId: string) { - const sortedColumns = this.columns - .getValue() - .slice() - .sort((columnA, columnB) => { - return columnA.id === columnId - ? -1 - : columnB.id === columnId - ? 1 - : columnA.sortOrder - columnB.sortOrder; - }); - - const sortedData = this.data - .getValue() - .slice() - .sort((a, b) => { - for (const column of sortedColumns) { - let returnValue = column.ascendingSortFunction(a, b); - if (column.sortDirection === SortDirection.desc) { - returnValue *= -1; - } - if (returnValue !== 0) return returnValue; + sortData( + data: Datum[], + primaryColumnSortId: string, + columns: TableColumn[] + ): Datum[] { + const sortedColumns = columns.slice().sort((columnA, columnB) => { + return columnA.id === primaryColumnSortId + ? -1 + : columnB.id === primaryColumnSortId + ? 1 + : columnA.sortOrder - columnB.sortOrder; + }); + + const sortedData = data.slice().sort((a, b) => { + for (const column of sortedColumns) { + let returnValue = column.ascendingSortFunction(a, b); + if (column.sortDirection === SortDirection.desc) { + returnValue *= -1; } - return 0; - }); + if (returnValue !== 0) return returnValue; + } + return 0; + }); + return sortedData; + } - sortedColumns[0].sortDirection = - sortedColumns[0].sortDirection == SortDirection.asc - ? SortDirection.desc - : SortDirection.asc; + // getMinSortOrderColumn(columns: TableColumn[]): TableColumn { + // const minSortOrder = min(columns, (x) => x.sortOrder); + // return columns.find((x) => x.sortOrder === minSortOrder); + // } - this.columns.next(sortedColumns); - this.data.next(sortedData); + getColumnsWithNewSortApplied( + activeSortColumnId: string, + columns: TableColumn[], + toggleSortDirection = true + ): TableColumn[] { + const columnsWithSortDir = columns.map((x) => { + if (x.id === activeSortColumnId) { + if (toggleSortDirection) { + x.sortDirection = + x.sortDirection === SortDirection.asc + ? SortDirection.desc + : SortDirection.asc; + } + x.activelySorted = true; + } else { + if (toggleSortDirection) { + x.sortDirection = x.initialSortDirection; + } + x.activelySorted = false; + } + return x; + }); + return columnsWithSortDir; + } + + handleSort(columnId: string) { + this.sortColId.next(columnId); } override disconnect(_collectionViewer: CollectionViewer): void {} override connect(_collectionViewer: CollectionViewer): Observable { - return this.data$; + return this.sortedData$; } } From f4c2c74c25f40bdb58765eb070402caa2da1d82a Mon Sep 17 00:00:00 2001 From: Ally Choung Date: Wed, 5 Feb 2025 16:03:10 -0500 Subject: [PATCH 06/47] fix: remove props from table column --- .../src/lib/table/table-column.ts | 26 ------------- .../src/lib/table/table.data-source.ts | 38 ++----------------- 2 files changed, 3 insertions(+), 61 deletions(-) diff --git a/libs/ui-components/src/lib/table/table-column.ts b/libs/ui-components/src/lib/table/table-column.ts index aca280f4b..b7e2b70b2 100644 --- a/libs/ui-components/src/lib/table/table-column.ts +++ b/libs/ui-components/src/lib/table/table-column.ts @@ -1,32 +1,15 @@ -import { ascending } from 'd3'; - export enum SortDirection { asc = 'asc', desc = 'desc', } export type SortDirectionType = keyof typeof SortDirection; -export type TableValue = string | number | boolean | Date; -export type TableCellAlignment = 'left' | 'center' | 'right'; export class TableColumn { /** * The id of the column. Used in the table header. * */ id: string; - /** - * Function to extract the value to be sorted on from the datum. - * If not provided, the formatted value will be used for sorting. - */ - getSortValue: (x: Datum) => TableValue; - /** - * Function to format the value for display in the table. - */ - getFormattedValue: (x: Datum) => string; - /** - * Function to determine the alignment of the cell content. - */ - getAlignment: (x: Datum) => TableCellAlignment; /** * Width of the column. Can be a percentage or pixel value. */ @@ -55,16 +38,7 @@ export class TableColumn { constructor(init?: Partial>) { this.sortDirection = SortDirection.asc; - this.getAlignment = () => 'left'; Object.assign(this, init); this.initialSortDirection = this.sortDirection; - if (this.ascendingSortFunction === undefined) { - this.ascendingSortFunction = this.defaultSort; - } - } - - defaultSort(a: Datum, b: Datum): number { - const accessor = this.getSortValue || this.getFormattedValue; - return ascending(accessor(a), accessor(b)); } } diff --git a/libs/ui-components/src/lib/table/table.data-source.ts b/libs/ui-components/src/lib/table/table.data-source.ts index 8f64c51c0..403d1c203 100644 --- a/libs/ui-components/src/lib/table/table.data-source.ts +++ b/libs/ui-components/src/lib/table/table.data-source.ts @@ -18,47 +18,15 @@ export class HsiUiTableDataSource extends DataSource { private sortColId = new BehaviorSubject(null); private sortColId$ = this.sortColId.asObservable(); - // TODO: get rid of subscribe, use rxjs operators instead - // TODO: clean up table-column.ts only use properties used in this class // TODO: add sort icon to table example // TODO: plan sort column directive constructor( private inputData$: Observable, - private inputColumns$: Observable[]> + private columns$: Observable[]> ) { super(); - // this.columns$ = this.inputColumns$ - // .getValue() - // .slice() - // .sort((columnA, columnB) => { - // return columnA.id === columnId - // ? -1 - // : columnB.id === columnId - // ? 1 - // : columnA.sortOrder - columnB.sortOrder; - // }); - - // const sortedData = this.data - // .getValue() - // .slice() - // .sort((a, b) => { - // for (const column of sortedColumns) { - // let returnValue = column.ascendingSortFunction(a, b); - // if (column.sortDirection === SortDirection.desc) { - // returnValue *= -1; - // } - // if (returnValue !== 0) return returnValue; - // } - // return 0; - // }); - - // sortedColumns[0].sortDirection = - // sortedColumns[0].sortDirection == SortDirection.asc - // ? SortDirection.desc - // : SortDirection.asc; - - const config$ = combineLatest([this.inputData$, this.inputColumns$]).pipe( + const config$ = combineLatest([this.inputData$, this.columns$]).pipe( withLatestFrom(this.sortColId$), map(([[data, cols], sortId]) => () => { // const activeSortColumn = @@ -100,7 +68,7 @@ export class HsiUiTableDataSource extends DataSource { map((x) => x.data), shareReplay(1) ); - this.inputColumns$ = sortedConfig$.pipe( + this.columns$ = sortedConfig$.pipe( map((x) => x.columns), shareReplay(1) ); From 74dfeb57df9c348638e615ec885ea8dbfb2ce058 Mon Sep 17 00:00:00 2001 From: Ally Choung Date: Thu, 6 Feb 2025 13:00:07 -0500 Subject: [PATCH 07/47] fix: remove old logic for new hsi ui table --- .../table-example.component.html | 33 +++++++------------ .../table-example/table-example.component.ts | 3 ++ .../src/lib/table/table-column.ts | 10 ++++++ .../src/lib/table/table.data-source.ts | 18 ++++------ 4 files changed, 31 insertions(+), 33 deletions(-) diff --git a/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.html b/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.html index cb431a307..33cf5151f 100644 --- a/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.html +++ b/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.html @@ -1,26 +1,17 @@ @if (columns$ | async; as columns) {
- - - - - - - - - + @for (name of ColumnNames | keyvalue; track name.key) { + + + + + } diff --git a/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.ts b/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.ts index 8debbc8e1..4577bb95e 100644 --- a/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.ts +++ b/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.ts @@ -23,6 +23,9 @@ enum ColumnNames { export class TableExampleComponent { columnIds = [ColumnNames.fruit, ColumnNames.color]; data$ = of([ + { fruit: 'lemon', color: 'yellow' }, + { fruit: 'mango', color: 'orange' }, + { fruit: 'avocado', color: 'green' }, { fruit: 'apple', color: 'red' }, { fruit: 'orange', color: 'orange' }, { fruit: 'banana', color: 'yellow' }, diff --git a/libs/ui-components/src/lib/table/table-column.ts b/libs/ui-components/src/lib/table/table-column.ts index b7e2b70b2..388bf987d 100644 --- a/libs/ui-components/src/lib/table/table-column.ts +++ b/libs/ui-components/src/lib/table/table-column.ts @@ -4,12 +4,22 @@ export enum SortDirection { } export type SortDirectionType = keyof typeof SortDirection; +export type TableValue = string | number | boolean | Date; export class TableColumn { /** * The id of the column. Used in the table header. * */ id: string; + /** + * Function to extract the value to be sorted on from the datum. + * If not provided, the formatted value will be used for sorting. + */ + getSortValue: (x: Datum) => TableValue; + /** + * Function to format the value for display in the table. + */ + getFormattedValue: (x: Datum) => string; /** * Width of the column. Can be a percentage or pixel value. */ diff --git a/libs/ui-components/src/lib/table/table.data-source.ts b/libs/ui-components/src/lib/table/table.data-source.ts index 403d1c203..ddf3629b8 100644 --- a/libs/ui-components/src/lib/table/table.data-source.ts +++ b/libs/ui-components/src/lib/table/table.data-source.ts @@ -1,4 +1,4 @@ -import { CollectionViewer, DataSource } from '@angular/cdk/collections'; +import { DataSource } from '@angular/cdk/collections'; import { BehaviorSubject, Observable, @@ -29,11 +29,9 @@ export class HsiUiTableDataSource extends DataSource { const config$ = combineLatest([this.inputData$, this.columns$]).pipe( withLatestFrom(this.sortColId$), map(([[data, cols], sortId]) => () => { - // const activeSortColumn = - // cols.find((c) => c.id == sortId) || this.getMinSortOrderColumn(cols); const columns = this.getColumnsWithNewSortApplied(sortId, cols, false); return { - data: this.sortData(data, sortId, columns), + data: sortId ? this.sortData(data, sortId, columns) : data, columns, }; }) @@ -46,7 +44,8 @@ export class HsiUiTableDataSource extends DataSource { (sortedConfig: { data: Datum[]; columns: TableColumn[] }) => { const columns = this.getColumnsWithNewSortApplied( sortId, - sortedConfig.columns + sortedConfig.columns, + true ); return { data: this.sortData(sortedConfig.data, sortId, columns), @@ -100,11 +99,6 @@ export class HsiUiTableDataSource extends DataSource { return sortedData; } - // getMinSortOrderColumn(columns: TableColumn[]): TableColumn { - // const minSortOrder = min(columns, (x) => x.sortOrder); - // return columns.find((x) => x.sortOrder === minSortOrder); - // } - getColumnsWithNewSortApplied( activeSortColumnId: string, columns: TableColumn[], @@ -134,9 +128,9 @@ export class HsiUiTableDataSource extends DataSource { this.sortColId.next(columnId); } - override disconnect(_collectionViewer: CollectionViewer): void {} + override disconnect(): void {} - override connect(_collectionViewer: CollectionViewer): Observable { + override connect(): Observable { return this.sortedData$; } } From ff28423724b98c755f9fa954159ace3e8241527a Mon Sep 17 00:00:00 2001 From: Ally Choung Date: Thu, 6 Feb 2025 14:35:11 -0500 Subject: [PATCH 08/47] fix: update to not sort automatically --- libs/ui-components/src/lib/table/table-column.ts | 5 ++++- libs/ui-components/src/lib/table/table.data-source.ts | 4 +++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/libs/ui-components/src/lib/table/table-column.ts b/libs/ui-components/src/lib/table/table-column.ts index 388bf987d..3262d988d 100644 --- a/libs/ui-components/src/lib/table/table-column.ts +++ b/libs/ui-components/src/lib/table/table-column.ts @@ -45,7 +45,10 @@ export class TableColumn { */ isRowHeader = false; readonly initialSortDirection: SortDirectionType; - + /** + * Whether the column data has been sorted since initialization. + */ + sortedOnInit = false; constructor(init?: Partial>) { this.sortDirection = SortDirection.asc; Object.assign(this, init); diff --git a/libs/ui-components/src/lib/table/table.data-source.ts b/libs/ui-components/src/lib/table/table.data-source.ts index ddf3629b8..ac787976f 100644 --- a/libs/ui-components/src/lib/table/table.data-source.ts +++ b/libs/ui-components/src/lib/table/table.data-source.ts @@ -106,11 +106,13 @@ export class HsiUiTableDataSource extends DataSource { ): TableColumn[] { const columnsWithSortDir = columns.map((x) => { if (x.id === activeSortColumnId) { - if (toggleSortDirection) { + if (toggleSortDirection && x.sortedOnInit) { x.sortDirection = x.sortDirection === SortDirection.asc ? SortDirection.desc : SortDirection.asc; + } else if (toggleSortDirection) { + x.sortedOnInit = true; } x.activelySorted = true; } else { From 6673cf036aeeeed7283c185585ffdff24ef1cd08 Mon Sep 17 00:00:00 2001 From: Ally Choung Date: Thu, 6 Feb 2025 15:42:52 -0500 Subject: [PATCH 09/47] fix: remove original table component files --- .../documentation-directory.yaml | 1 - libs/ui-components/src/lib/table/index.ts | 1 - .../single-sort-header.component.spec.ts | 2 - .../src/lib/table/table.component.html | 69 ------ .../src/lib/table/table.component.spec.ts | 224 +++++++++--------- .../src/lib/table/table.component.ts | 183 -------------- .../src/lib/table/table.module.ts | 5 +- 7 files changed, 114 insertions(+), 371 deletions(-) delete mode 100644 libs/ui-components/src/lib/table/table.component.html delete mode 100644 libs/ui-components/src/lib/table/table.component.ts diff --git a/apps/demo-app/src/assets/ui-components/documentation/documentation-directory.yaml b/apps/demo-app/src/assets/ui-components/documentation/documentation-directory.yaml index ddc3eb26f..01e60a206 100644 --- a/apps/demo-app/src/assets/ui-components/documentation/documentation-directory.yaml +++ b/apps/demo-app/src/assets/ui-components/documentation/documentation-directory.yaml @@ -14,7 +14,6 @@ directory: directory-item: 'interfaces/HsiUiDirectoryItem.html' directory-selection: 'interfaces/HsiUiDirectorySelection.html' table: - table: 'components/TableComponent.html' table-config: 'classes/HsiUiTableConfig.html' table-data-source: 'classes/HsiUiTableDataSource.html' table-column: 'classes/TableColumn.html' diff --git a/libs/ui-components/src/lib/table/index.ts b/libs/ui-components/src/lib/table/index.ts index 8959328b8..4e213856a 100644 --- a/libs/ui-components/src/lib/table/index.ts +++ b/libs/ui-components/src/lib/table/index.ts @@ -1,5 +1,4 @@ export * from './table-column'; -export * from './table.component'; export * from './table.config'; export * from './table.data-source'; export * from './table.module'; diff --git a/libs/ui-components/src/lib/table/single-sort-header/single-sort-header.component.spec.ts b/libs/ui-components/src/lib/table/single-sort-header/single-sort-header.component.spec.ts index 41a312609..7693f1d54 100644 --- a/libs/ui-components/src/lib/table/single-sort-header/single-sort-header.component.spec.ts +++ b/libs/ui-components/src/lib/table/single-sort-header/single-sort-header.component.spec.ts @@ -2,7 +2,6 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { SortDirection, TableColumn } from '../table-column'; -import { TableComponent } from '../table.component'; import { SingleSortHeaderComponent } from './single-sort-header.component'; describe('SingleSortHeaderComponent', () => { @@ -12,7 +11,6 @@ describe('SingleSortHeaderComponent', () => { beforeEach(() => { TestBed.configureTestingModule({ declarations: [SingleSortHeaderComponent], - providers: [TableComponent], }); fixture = TestBed.createComponent(SingleSortHeaderComponent); component = fixture.componentInstance; diff --git a/libs/ui-components/src/lib/table/table.component.html b/libs/ui-components/src/lib/table/table.component.html deleted file mode 100644 index bcde6f0fe..000000000 --- a/libs/ui-components/src/lib/table/table.component.html +++ /dev/null @@ -1,69 +0,0 @@ - diff --git a/libs/ui-components/src/lib/table/table.component.spec.ts b/libs/ui-components/src/lib/table/table.component.spec.ts index 87ea26bb4..ba6674195 100644 --- a/libs/ui-components/src/lib/table/table.component.spec.ts +++ b/libs/ui-components/src/lib/table/table.component.spec.ts @@ -1,121 +1,121 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { of, tap } from 'rxjs'; -import { TestScheduler } from 'rxjs/testing'; -import { SortDirection, TableColumn } from './table-column'; -import { TableComponent } from './table.component'; +// /* eslint-disable @typescript-eslint/no-explicit-any */ +// import { CdkTable } from '@angular/cdk/table'; +// import { ComponentFixture, TestBed } from '@angular/core/testing'; +// import { of, tap } from 'rxjs'; +// import { TestScheduler } from 'rxjs/testing'; +// import { SortDirection, TableColumn } from './table-column'; -describe('TableComponent', () => { - let component: TableComponent; - let fixture: ComponentFixture>; - let testScheduler: TestScheduler; +// describe('TableComponent', () => { +// let component: CdkTable; +// let fixture: ComponentFixture>; +// let testScheduler: TestScheduler; - beforeEach(() => { - TestBed.configureTestingModule({ - declarations: [TableComponent], - }); - fixture = TestBed.createComponent(TableComponent); - testScheduler = new TestScheduler((actual, expected) => { - return expect(actual).toEqual(expected); - }); - component = fixture.componentInstance; - }); +// beforeEach(() => { +// TestBed.configureTestingModule({ +// declarations: [CdkTable], +// }); +// fixture = TestBed.createComponent(CdkTable); +// testScheduler = new TestScheduler((actual, expected) => { +// return expect(actual).toEqual(expected); +// }); +// component = fixture.componentInstance; +// }); - describe('ngOnInit', () => { - beforeEach(() => { - spyOn(component, 'setTableData'); - spyOn(component, 'setTableHeaders'); - spyOn(component, 'validateRowHeaders'); - }); - it('should call setTableData', () => { - component.ngOnInit(); - expect(component.setTableData).toHaveBeenCalled(); - }); - it('should call setTableHeaders', () => { - component.ngOnInit(); - expect(component.setTableHeaders).toHaveBeenCalled(); - }); - it('should call validateRowHeaders', () => { - component.ngOnInit(); - expect(component.validateRowHeaders).toHaveBeenCalled(); - }); - }); +// describe('ngOnInit', () => { +// beforeEach(() => { +// spyOn(component, 'setTableData'); +// spyOn(component, 'setTableHeaders'); +// spyOn(component, 'validateRowHeaders'); +// }); +// it('should call setTableData', () => { +// component.ngOnInit(); +// expect(component.setTableData).toHaveBeenCalled(); +// }); +// it('should call setTableHeaders', () => { +// component.ngOnInit(); +// expect(component.setTableHeaders).toHaveBeenCalled(); +// }); +// it('should call validateRowHeaders', () => { +// component.ngOnInit(); +// expect(component.validateRowHeaders).toHaveBeenCalled(); +// }); +// }); - describe('sortTableByColumn', () => { - it('should emit column', () => { - spyOn(component.sort, 'next'); - const column = { name: 'test', sort: SortDirection.asc }; - component.sortTableByColumn(column as any); - expect(component.sort.next).toHaveBeenCalledWith(column as any); - }); - }); +// describe('sortTableByColumn', () => { +// it('should emit column', () => { +// spyOn(component.sort, 'next'); +// const column = { name: 'test', sort: SortDirection.asc }; +// component.sortTableByColumn(column as any); +// expect(component.sort.next).toHaveBeenCalledWith(column as any); +// }); +// }); - describe('integration test: single sort column', () => { - it('correctly sorts data', () => { - const originalData = [ - { name: 'a', tiebreak: 0 }, - { name: 'a', tiebreak: 1 }, - { name: 'b', tiebreak: 0 }, - { name: 'd', tiebreak: 1 }, - { name: 'd', tiebreak: 0 }, - ]; - const chosenColumn = new TableColumn<{ - name: string; - tiebreak: number; - }>({ - getSortValue: (x) => x.name, - sortDirection: SortDirection.asc, - id: 'name', - sortOrder: 1, - }); - const columns = [ - chosenColumn, - new TableColumn<{ - name: string; - tiebreak: number; - }>({ - getSortValue: (x) => x.tiebreak, - sortDirection: SortDirection.asc, - sortOrder: 2, - }), - ]; - component.config$ = of({ data: originalData, columns: columns }); - component.ngOnInit(); +// describe('integration test: single sort column', () => { +// it('correctly sorts data', () => { +// const originalData = [ +// { name: 'a', tiebreak: 0 }, +// { name: 'a', tiebreak: 1 }, +// { name: 'b', tiebreak: 0 }, +// { name: 'd', tiebreak: 1 }, +// { name: 'd', tiebreak: 0 }, +// ]; +// const chosenColumn = new TableColumn<{ +// name: string; +// tiebreak: number; +// }>({ +// getSortValue: (x) => x.name, +// sortDirection: SortDirection.asc, +// id: 'name', +// sortOrder: 1, +// }); +// const columns = [ +// chosenColumn, +// new TableColumn<{ +// name: string; +// tiebreak: number; +// }>({ +// getSortValue: (x) => x.tiebreak, +// sortDirection: SortDirection.asc, +// sortOrder: 2, +// }), +// ]; +// component.config$ = of({ data: originalData, columns: columns }); +// component.ngOnInit(); - const ascData = [ - { name: 'a', tiebreak: 0 }, - { name: 'a', tiebreak: 1 }, - { name: 'b', tiebreak: 0 }, - { name: 'd', tiebreak: 0 }, - { name: 'd', tiebreak: 1 }, - ]; +// const ascData = [ +// { name: 'a', tiebreak: 0 }, +// { name: 'a', tiebreak: 1 }, +// { name: 'b', tiebreak: 0 }, +// { name: 'd', tiebreak: 0 }, +// { name: 'd', tiebreak: 1 }, +// ]; - const descData = [ - { name: 'd', tiebreak: 0 }, - { name: 'd', tiebreak: 1 }, - { name: 'b', tiebreak: 0 }, - { name: 'a', tiebreak: 0 }, - { name: 'a', tiebreak: 1 }, - ]; +// const descData = [ +// { name: 'd', tiebreak: 0 }, +// { name: 'd', tiebreak: 1 }, +// { name: 'b', tiebreak: 0 }, +// { name: 'a', tiebreak: 0 }, +// { name: 'a', tiebreak: 1 }, +// ]; - const expectedMarbles = 'abc'; - const triggerMarbles = ' -xy|'; - const expectedValues = { - a: ascData, // correctly emits sorted data on first emission - b: descData, // flips correctly - c: ascData, // flops correctly - }; - const triggerValues = { - x: () => component.sortTableByColumn(chosenColumn), - y: () => component.sortTableByColumn(chosenColumn), - }; +// const expectedMarbles = 'abc'; +// const triggerMarbles = ' -xy|'; +// const expectedValues = { +// a: ascData, // correctly emits sorted data on first emission +// b: descData, // flips correctly +// c: ascData, // flops correctly +// }; +// const triggerValues = { +// x: () => component.sortTableByColumn(chosenColumn), +// y: () => component.sortTableByColumn(chosenColumn), +// }; - testScheduler.run(({ expectObservable, cold }) => { - expectObservable(component.data$).toBe(expectedMarbles, expectedValues); - expectObservable( - cold(triggerMarbles, triggerValues).pipe(tap((fn) => fn())) - ); - }); - }); - }); -}); +// testScheduler.run(({ expectObservable, cold }) => { +// expectObservable(component.data$).toBe(expectedMarbles, expectedValues); +// expectObservable( +// cold(triggerMarbles, triggerValues).pipe(tap((fn) => fn())) +// ); +// }); +// }); +// }); +// }); diff --git a/libs/ui-components/src/lib/table/table.component.ts b/libs/ui-components/src/lib/table/table.component.ts deleted file mode 100644 index b38e63fcb..000000000 --- a/libs/ui-components/src/lib/table/table.component.ts +++ /dev/null @@ -1,183 +0,0 @@ -import { - ChangeDetectionStrategy, - Component, - DestroyRef, - Input, - OnInit, - ViewEncapsulation, -} from '@angular/core'; -import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; -import { min } from 'd3'; -import { isEqual } from 'lodash-es'; -import { - BehaviorSubject, - Observable, - distinctUntilChanged, - filter, - map, - merge, - scan, - shareReplay, - withLatestFrom, -} from 'rxjs'; -import { SortDirection, TableColumn } from './table-column'; -import { HsiUiTableConfig } from './table.config'; - -@Component({ - selector: 'hsi-ui-table', - templateUrl: './table.component.html', - styleUrls: ['./table.component.scss'], - encapsulation: ViewEncapsulation.None, - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class TableComponent implements OnInit { - @Input() config$: Observable>; - @Input() sortIcon = 'arrow_upward'; - data$: Observable; - columns$: Observable[]>; - tableHeaders$: Observable; - sort: BehaviorSubject> = new BehaviorSubject(null); - sort$: Observable> = this.sort.asObservable(); - - constructor(private destroyRef: DestroyRef) {} - - ngOnInit(): void { - this.setTableData(); - this.setTableHeaders(); - this.validateRowHeaders(); - } - - setTableData(): void { - const config$ = this.config$.pipe( - withLatestFrom(this.sort$), - map(([config, sort]) => () => { - const activeSortColumn = - sort || this.getMinSortOrderColumn(config.columns); - const columns = this.getColumnsWithNewSortApplied( - activeSortColumn, - config.columns, - false - ); - return { - data: this.sortData(config.data, activeSortColumn, columns), - columns, - }; - }) - ); - - const sort$ = this.sort$.pipe( - filter((sort) => sort !== null), - map((sort) => (sortedConfig: HsiUiTableConfig) => { - const columns = this.getColumnsWithNewSortApplied( - sort, - sortedConfig.columns - ); - return { - data: this.sortData(sortedConfig.data, sort, columns), - columns, - }; - }) - ); - const sortedConfig$ = merge(config$, sort$).pipe( - scan((sortedConfig, changeFn) => changeFn(sortedConfig), { - data: [], - columns: [], - }), - shareReplay(1) // do not remove sort toggle will be called twice - ); - - this.data$ = sortedConfig$.pipe( - map((x) => x.data), - shareReplay(1) - ); - this.columns$ = sortedConfig$.pipe( - map((x) => x.columns), - shareReplay(1) - ); - } - - getMinSortOrderColumn(columns: TableColumn[]): TableColumn { - const minSortOrder = min(columns, (x) => x.sortOrder); - return columns.find((x) => x.sortOrder === minSortOrder); - } - - getColumnsWithNewSortApplied( - activeSortColumn: TableColumn, - columns: TableColumn[], - toggleSortDirection = true - ): TableColumn[] { - const columnsWithSortDir = columns.map((x) => { - if (x.id === activeSortColumn.id) { - if (toggleSortDirection) { - x.sortDirection = - x.sortDirection === SortDirection.asc - ? SortDirection.desc - : SortDirection.asc; - } - x.activelySorted = true; - } else { - if (toggleSortDirection) { - x.sortDirection = x.initialSortDirection; - } - x.activelySorted = false; - } - return x; - }); - return columnsWithSortDir; - } - - setTableHeaders(): void { - this.tableHeaders$ = this.columns$.pipe( - map((columns) => columns.map((x) => x.id)), - distinctUntilChanged((a, b) => isEqual(a, b)), - shareReplay(1) - ); - } - - validateRowHeaders(): void { - this.columns$ - .pipe(takeUntilDestroyed(this.destroyRef)) - .subscribe((columns) => { - const rowHeaders = columns.filter((x) => x.isRowHeader); - if (rowHeaders.length > 1) { - throw new Error( - 'Table can only have one row header column. Please update your column config.' - ); - } - }); - } - - sortTableByColumn(column: TableColumn) { - this.sort.next(column); - } - - sortData( - data: Datum[], - primaryColumnSort: TableColumn, - columns: TableColumn[] - ): Datum[] { - const sortedColumns = columns.slice().sort((columnA, columnB) => { - return columnA.id === primaryColumnSort.id - ? -1 - : columnB.id === primaryColumnSort.id - ? 1 - : columnA.sortOrder - columnB.sortOrder; - }); - - const sortedData = data.slice().sort((a, b) => { - for (const column of sortedColumns) { - let returnValue = column.ascendingSortFunction(a, b); - if (column.sortDirection === SortDirection.desc) { - returnValue *= -1; - } - if (returnValue !== 0) return returnValue; - } - return 0; - }); - return sortedData; - } - - columnTrackingFunction(_: number, column: TableColumn) { - return column.id; - } -} diff --git a/libs/ui-components/src/lib/table/table.module.ts b/libs/ui-components/src/lib/table/table.module.ts index 46cd03841..12618f611 100644 --- a/libs/ui-components/src/lib/table/table.module.ts +++ b/libs/ui-components/src/lib/table/table.module.ts @@ -3,11 +3,10 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { MatIconModule } from '@angular/material/icon'; import { SingleSortHeaderComponent } from './single-sort-header/single-sort-header.component'; -import { TableComponent } from './table.component'; @NgModule({ - declarations: [TableComponent, SingleSortHeaderComponent], + declarations: [SingleSortHeaderComponent], imports: [CommonModule, CdkTableModule, MatIconModule], - exports: [TableComponent, CdkTableModule], + exports: [CdkTableModule], }) export class TableModule {} From 9b062a14dfb43c9102307cfd52292380aa859ae6 Mon Sep 17 00:00:00 2001 From: Ally Choung Date: Fri, 7 Feb 2025 10:46:57 -0500 Subject: [PATCH 10/47] fix: update table example styling, use single sort header --- .../table-example.component.html | 35 +++++--- .../table-example.component.scss | 81 +++++++++++++++++++ .../table-example/table-example.component.ts | 21 ++++- libs/ui-components/src/lib/table/index.ts | 1 + .../src/lib/table/table.module.ts | 2 +- 5 files changed, 125 insertions(+), 15 deletions(-) diff --git a/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.html b/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.html index 33cf5151f..d6b470a62 100644 --- a/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.html +++ b/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.html @@ -1,18 +1,29 @@ @if (columns$ | async; as columns) { -
- Fruit - {{ element.fruit }} - Color - {{ element.color }} + {{ name.value }} + {{ element[name.key] }}
- @for (name of ColumnNames | keyvalue; track name.key) { - - - +
- {{ name.value }} - {{ element[name.key] }}
+ @for (column of columns; track columnTrackingFunction) { + + @if (column.sortable) { + + } @else { + + } + } -
+ {{ column.id }} + {{ column.id }} + + {{ column.getFormattedValue(element) }} +
diff --git a/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.scss b/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.scss index e69de29bb..fa03764f0 100644 --- a/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.scss +++ b/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.scss @@ -0,0 +1,81 @@ +@use 'vars' as *; +@use 'functions' as *; +@use 'colors'; + +$icon-width: 0.9rem; +$icon-left-margin: 0.2rem; +$icon-right-margin: 0.4rem; +.table-container { + border-spacing: 0; + + td:last-child { + &.left { + padding-right: 0; + } + } + + th:last-child { + &.left { + padding-right: 0; + } + } + + th { + vertical-align: bottom; + &.sorted-header { + padding-right: 0; + } + } + + .header-cell-sort { + display: flex; + align-items: flex-end; + &:hover { + cursor: pointer; + } + } + + .material-symbols-outlined { + display: flex; + justify-content: center; + width: $icon-width; + height: 1.2rem; + font-size: 1.25rem; + margin-left: $icon-left-margin; + margin-right: $icon-right-margin; + opacity: 0.25; + transition: all 150ms ease-in-out; + + &:hover { + opacity: 0.75; + } + + &.actively-sorted { + opacity: 1; + } + } + + .desc { + transform: rotate(180deg); + } + + .left { + text-align: left; + } + + .right { + text-align: right; + + .header-cell-sort { + justify-content: flex-end; + } + + &.sorted-cell { + padding-right: $icon-left-margin + $icon-width + $icon-right-margin; + } + } + + .center { + text-align: center; + } +} diff --git a/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.ts b/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.ts index 4577bb95e..168526131 100644 --- a/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.ts +++ b/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.ts @@ -1,5 +1,10 @@ import { CommonModule } from '@angular/common'; -import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { + ChangeDetectionStrategy, + Component, + Input, + ViewEncapsulation, +} from '@angular/core'; import { HsiUiTableDataSource, TableColumn, @@ -17,10 +22,12 @@ enum ColumnNames { standalone: true, imports: [CommonModule, TableModule], templateUrl: './table-example.component.html', - styleUrl: './table-example.component.scss', + styleUrls: ['../../../examples.scss', './table-example.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, + encapsulation: ViewEncapsulation.None, }) export class TableExampleComponent { + @Input() sortIcon: string = 'arrow_upward'; columnIds = [ColumnNames.fruit, ColumnNames.color]; data$ = of([ { fruit: 'lemon', color: 'yellow' }, @@ -37,13 +44,23 @@ export class TableExampleComponent { id: ColumnNames.fruit, ascendingSortFunction: (a, b) => a.fruit.localeCompare(b.fruit), sortOrder: 1, + sortable: true, sortDirection: 'asc', // initial sort direction + getFormattedValue: (x) => x.fruit, + width: '80px', }), new TableColumn<{ fruit: string; color: string }>({ id: ColumnNames.color, sortable: true, ascendingSortFunction: (a, b) => a.color.localeCompare(b.color), + getFormattedValue: (x) => x.color, }), ]); dataSource = new HsiUiTableDataSource(this.data$, this.columns$); + columnTrackingFunction( + _: number, + column: TableColumn<{ fruit: string; color: string }> + ) { + return column.id; + } } diff --git a/libs/ui-components/src/lib/table/index.ts b/libs/ui-components/src/lib/table/index.ts index 4e213856a..2035925b3 100644 --- a/libs/ui-components/src/lib/table/index.ts +++ b/libs/ui-components/src/lib/table/index.ts @@ -1,3 +1,4 @@ +export * from './single-sort-header/single-sort-header.component'; export * from './table-column'; export * from './table.config'; export * from './table.data-source'; diff --git a/libs/ui-components/src/lib/table/table.module.ts b/libs/ui-components/src/lib/table/table.module.ts index 12618f611..e50577d28 100644 --- a/libs/ui-components/src/lib/table/table.module.ts +++ b/libs/ui-components/src/lib/table/table.module.ts @@ -7,6 +7,6 @@ import { SingleSortHeaderComponent } from './single-sort-header/single-sort-head @NgModule({ declarations: [SingleSortHeaderComponent], imports: [CommonModule, CdkTableModule, MatIconModule], - exports: [CdkTableModule], + exports: [CdkTableModule, SingleSortHeaderComponent], }) export class TableModule {} From 04d6590642107b2ea232e7d7932ff3de60cdedb6 Mon Sep 17 00:00:00 2001 From: Ally Choung Date: Mon, 10 Feb 2025 13:58:02 -0500 Subject: [PATCH 11/47] fix: update tests for data source --- .../src/lib/table/table-column.ts | 10 ++ .../src/lib/table/table.component.spec.ts | 121 ------------------ .../src/lib/table/table.data-source.spec.ts | 86 +++++++++++++ 3 files changed, 96 insertions(+), 121 deletions(-) delete mode 100644 libs/ui-components/src/lib/table/table.component.spec.ts create mode 100644 libs/ui-components/src/lib/table/table.data-source.spec.ts diff --git a/libs/ui-components/src/lib/table/table-column.ts b/libs/ui-components/src/lib/table/table-column.ts index 3262d988d..4a8e88a20 100644 --- a/libs/ui-components/src/lib/table/table-column.ts +++ b/libs/ui-components/src/lib/table/table-column.ts @@ -1,3 +1,5 @@ +import { ascending } from 'd3'; + export enum SortDirection { asc = 'asc', desc = 'desc', @@ -53,5 +55,13 @@ export class TableColumn { this.sortDirection = SortDirection.asc; Object.assign(this, init); this.initialSortDirection = this.sortDirection; + if (this.ascendingSortFunction === undefined) { + this.ascendingSortFunction = this.defaultSort; + } + } + + defaultSort(a: Datum, b: Datum): number { + const accessor = this.getSortValue || this.getFormattedValue; + return ascending(accessor(a), accessor(b)); } } diff --git a/libs/ui-components/src/lib/table/table.component.spec.ts b/libs/ui-components/src/lib/table/table.component.spec.ts deleted file mode 100644 index ba6674195..000000000 --- a/libs/ui-components/src/lib/table/table.component.spec.ts +++ /dev/null @@ -1,121 +0,0 @@ -// /* eslint-disable @typescript-eslint/no-explicit-any */ -// import { CdkTable } from '@angular/cdk/table'; -// import { ComponentFixture, TestBed } from '@angular/core/testing'; -// import { of, tap } from 'rxjs'; -// import { TestScheduler } from 'rxjs/testing'; -// import { SortDirection, TableColumn } from './table-column'; - -// describe('TableComponent', () => { -// let component: CdkTable; -// let fixture: ComponentFixture>; -// let testScheduler: TestScheduler; - -// beforeEach(() => { -// TestBed.configureTestingModule({ -// declarations: [CdkTable], -// }); -// fixture = TestBed.createComponent(CdkTable); -// testScheduler = new TestScheduler((actual, expected) => { -// return expect(actual).toEqual(expected); -// }); -// component = fixture.componentInstance; -// }); - -// describe('ngOnInit', () => { -// beforeEach(() => { -// spyOn(component, 'setTableData'); -// spyOn(component, 'setTableHeaders'); -// spyOn(component, 'validateRowHeaders'); -// }); -// it('should call setTableData', () => { -// component.ngOnInit(); -// expect(component.setTableData).toHaveBeenCalled(); -// }); -// it('should call setTableHeaders', () => { -// component.ngOnInit(); -// expect(component.setTableHeaders).toHaveBeenCalled(); -// }); -// it('should call validateRowHeaders', () => { -// component.ngOnInit(); -// expect(component.validateRowHeaders).toHaveBeenCalled(); -// }); -// }); - -// describe('sortTableByColumn', () => { -// it('should emit column', () => { -// spyOn(component.sort, 'next'); -// const column = { name: 'test', sort: SortDirection.asc }; -// component.sortTableByColumn(column as any); -// expect(component.sort.next).toHaveBeenCalledWith(column as any); -// }); -// }); - -// describe('integration test: single sort column', () => { -// it('correctly sorts data', () => { -// const originalData = [ -// { name: 'a', tiebreak: 0 }, -// { name: 'a', tiebreak: 1 }, -// { name: 'b', tiebreak: 0 }, -// { name: 'd', tiebreak: 1 }, -// { name: 'd', tiebreak: 0 }, -// ]; -// const chosenColumn = new TableColumn<{ -// name: string; -// tiebreak: number; -// }>({ -// getSortValue: (x) => x.name, -// sortDirection: SortDirection.asc, -// id: 'name', -// sortOrder: 1, -// }); -// const columns = [ -// chosenColumn, -// new TableColumn<{ -// name: string; -// tiebreak: number; -// }>({ -// getSortValue: (x) => x.tiebreak, -// sortDirection: SortDirection.asc, -// sortOrder: 2, -// }), -// ]; -// component.config$ = of({ data: originalData, columns: columns }); -// component.ngOnInit(); - -// const ascData = [ -// { name: 'a', tiebreak: 0 }, -// { name: 'a', tiebreak: 1 }, -// { name: 'b', tiebreak: 0 }, -// { name: 'd', tiebreak: 0 }, -// { name: 'd', tiebreak: 1 }, -// ]; - -// const descData = [ -// { name: 'd', tiebreak: 0 }, -// { name: 'd', tiebreak: 1 }, -// { name: 'b', tiebreak: 0 }, -// { name: 'a', tiebreak: 0 }, -// { name: 'a', tiebreak: 1 }, -// ]; - -// const expectedMarbles = 'abc'; -// const triggerMarbles = ' -xy|'; -// const expectedValues = { -// a: ascData, // correctly emits sorted data on first emission -// b: descData, // flips correctly -// c: ascData, // flops correctly -// }; -// const triggerValues = { -// x: () => component.sortTableByColumn(chosenColumn), -// y: () => component.sortTableByColumn(chosenColumn), -// }; - -// testScheduler.run(({ expectObservable, cold }) => { -// expectObservable(component.data$).toBe(expectedMarbles, expectedValues); -// expectObservable( -// cold(triggerMarbles, triggerValues).pipe(tap((fn) => fn())) -// ); -// }); -// }); -// }); -// }); diff --git a/libs/ui-components/src/lib/table/table.data-source.spec.ts b/libs/ui-components/src/lib/table/table.data-source.spec.ts new file mode 100644 index 000000000..70dfaedcb --- /dev/null +++ b/libs/ui-components/src/lib/table/table.data-source.spec.ts @@ -0,0 +1,86 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { TestBed } from '@angular/core/testing'; +import { of, tap } from 'rxjs'; +import { TestScheduler } from 'rxjs/testing'; +import { SortDirection, TableColumn } from './table-column'; +import { HsiUiTableDataSource } from './table.data-source'; + +describe('DataSource', () => { + let testScheduler: TestScheduler; + + beforeEach(() => { + TestBed.configureTestingModule({}); + testScheduler = new TestScheduler((actual, expected) => { + return expect(actual).toEqual(expected); + }); + }); + + describe('integration test: single sort column', () => { + it('correctly sorts data', () => { + const originalData = [ + { name: 'a', tiebreak: 0 }, + { name: 'a', tiebreak: 1 }, + { name: 'b', tiebreak: 0 }, + { name: 'd', tiebreak: 1 }, + { name: 'd', tiebreak: 0 }, + ]; + const chosenColumn = new TableColumn<{ + name: string; + tiebreak: number; + }>({ + getSortValue: (x) => x.name, + sortDirection: SortDirection.asc, + sortable: true, + id: 'name', + sortOrder: 1, + }); + const dataSource = new HsiUiTableDataSource( + of(originalData), + of([ + chosenColumn, + new TableColumn<{ + name: string; + tiebreak: number; + }>({ + getSortValue: (x) => x.tiebreak, + sortOrder: 2, + }), + ]) + ); + const connection$ = dataSource.connect(); + const ascData = [ + { name: 'a', tiebreak: 0 }, + { name: 'a', tiebreak: 1 }, + { name: 'b', tiebreak: 0 }, + { name: 'd', tiebreak: 0 }, + { name: 'd', tiebreak: 1 }, + ]; + + const descData = [ + { name: 'd', tiebreak: 0 }, + { name: 'd', tiebreak: 1 }, + { name: 'b', tiebreak: 0 }, + { name: 'a', tiebreak: 0 }, + { name: 'a', tiebreak: 1 }, + ]; + + const expectedMarbles = 'abc'; + const triggerMarbles = ' -xy|'; + const expectedValues = { + a: originalData, // correctly emits non-sorted data on first emission + b: ascData, // sorts correctly + c: descData, // flips correctly + }; + const triggerValues = { + x: () => dataSource.handleSort(chosenColumn.id), + y: () => dataSource.handleSort(chosenColumn.id), + }; + testScheduler.run(({ expectObservable, cold }) => { + expectObservable(connection$).toBe(expectedMarbles, expectedValues); + expectObservable( + cold(triggerMarbles, triggerValues).pipe(tap((fn) => fn())) + ); + }); + }); + }); +}); From 38f49ad4b65ebd805a714fecbf289520ac5ec82f Mon Sep 17 00:00:00 2001 From: Ally Choung Date: Mon, 10 Feb 2025 14:21:51 -0500 Subject: [PATCH 12/47] fix: remove duplicate styling, remove width prop from table col --- .../table-example.component.scss | 55 ------------- .../table-example/table-example.component.ts | 1 - .../single-sort-header.component.scss | 55 +++++++++++++ .../src/lib/table/table-column.ts | 8 -- .../src/lib/table/table.component.scss | 77 ------------------- 5 files changed, 55 insertions(+), 141 deletions(-) delete mode 100644 libs/ui-components/src/lib/table/table.component.scss diff --git a/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.scss b/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.scss index fa03764f0..12534496e 100644 --- a/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.scss +++ b/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.scss @@ -2,9 +2,6 @@ @use 'functions' as *; @use 'colors'; -$icon-width: 0.9rem; -$icon-left-margin: 0.2rem; -$icon-right-margin: 0.4rem; .table-container { border-spacing: 0; @@ -26,56 +23,4 @@ $icon-right-margin: 0.4rem; padding-right: 0; } } - - .header-cell-sort { - display: flex; - align-items: flex-end; - &:hover { - cursor: pointer; - } - } - - .material-symbols-outlined { - display: flex; - justify-content: center; - width: $icon-width; - height: 1.2rem; - font-size: 1.25rem; - margin-left: $icon-left-margin; - margin-right: $icon-right-margin; - opacity: 0.25; - transition: all 150ms ease-in-out; - - &:hover { - opacity: 0.75; - } - - &.actively-sorted { - opacity: 1; - } - } - - .desc { - transform: rotate(180deg); - } - - .left { - text-align: left; - } - - .right { - text-align: right; - - .header-cell-sort { - justify-content: flex-end; - } - - &.sorted-cell { - padding-right: $icon-left-margin + $icon-width + $icon-right-margin; - } - } - - .center { - text-align: center; - } } diff --git a/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.ts b/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.ts index 168526131..baa57d6aa 100644 --- a/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.ts +++ b/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.ts @@ -47,7 +47,6 @@ export class TableExampleComponent { sortable: true, sortDirection: 'asc', // initial sort direction getFormattedValue: (x) => x.fruit, - width: '80px', }), new TableColumn<{ fruit: string; color: string }>({ id: ColumnNames.color, diff --git a/libs/ui-components/src/lib/table/single-sort-header/single-sort-header.component.scss b/libs/ui-components/src/lib/table/single-sort-header/single-sort-header.component.scss index e69de29bb..1e02219c9 100644 --- a/libs/ui-components/src/lib/table/single-sort-header/single-sort-header.component.scss +++ b/libs/ui-components/src/lib/table/single-sort-header/single-sort-header.component.scss @@ -0,0 +1,55 @@ +$icon-width: 0.9rem; +$icon-left-margin: 0.2rem; +$icon-right-margin: 0.4rem; + +.header-cell-sort { + display: flex; + align-items: flex-end; + &:hover { + cursor: pointer; + } +} + +.material-symbols-outlined { + display: flex; + justify-content: center; + width: $icon-width; + height: 1.2rem; + font-size: 1.25rem; + margin-left: $icon-left-margin; + margin-right: $icon-right-margin; + opacity: 0.25; + transition: all 150ms ease-in-out; + + &:hover { + opacity: 0.75; + } + + &.actively-sorted { + opacity: 1; + } +} + +.desc { + transform: rotate(180deg); +} + +.left { + text-align: left; +} + +.right { + text-align: right; + + .header-cell-sort { + justify-content: flex-end; + } + + &.sorted-cell { + padding-right: $icon-left-margin + $icon-width + $icon-right-margin; + } +} + +.center { + text-align: center; +} diff --git a/libs/ui-components/src/lib/table/table-column.ts b/libs/ui-components/src/lib/table/table-column.ts index 4a8e88a20..2e77e9cef 100644 --- a/libs/ui-components/src/lib/table/table-column.ts +++ b/libs/ui-components/src/lib/table/table-column.ts @@ -22,10 +22,6 @@ export class TableColumn { * Function to format the value for display in the table. */ getFormattedValue: (x: Datum) => string; - /** - * Width of the column. Can be a percentage or pixel value. - */ - width: string; /** * Function to determine the sort order of the column. * If not provided, sort with use d3.ascending on the getSortValue or getFormattedValue. @@ -42,10 +38,6 @@ export class TableColumn { * Sorting tiebreaks are determined by increasing sortOrder number. **/ sortOrder: number = Number.MAX_SAFE_INTEGER; - /** - * Whether the column is a row header. - */ - isRowHeader = false; readonly initialSortDirection: SortDirectionType; /** * Whether the column data has been sorted since initialization. diff --git a/libs/ui-components/src/lib/table/table.component.scss b/libs/ui-components/src/lib/table/table.component.scss deleted file mode 100644 index 935ed9e94..000000000 --- a/libs/ui-components/src/lib/table/table.component.scss +++ /dev/null @@ -1,77 +0,0 @@ -$icon-width: 0.9rem; -$icon-left-margin: 0.2rem; -$icon-right-margin: 0.4rem; -.table-container { - border-spacing: 0; - - td:last-child { - &.left { - padding-right: 0; - } - } - - th:last-child { - &.left { - padding-right: 0; - } - } - - th { - vertical-align: bottom; - &.sorted-header { - padding-right: 0; - } - } - - .header-cell-sort { - display: flex; - align-items: flex-end; - &:hover { - cursor: pointer; - } - } - - .material-symbols-outlined { - display: flex; - justify-content: center; - width: $icon-width; - height: 1.2rem; - font-size: 1.25rem; - margin-left: $icon-left-margin; - margin-right: $icon-right-margin; - opacity: 0.25; - transition: all 150ms ease-in-out; - - &:hover { - opacity: 0.75; - } - - &.actively-sorted { - opacity: 1; - } - } - - .desc { - transform: rotate(180deg); - } - - .left { - text-align: left; - } - - .right { - text-align: right; - - .header-cell-sort { - justify-content: flex-end; - } - - &.sorted-cell { - padding-right: $icon-left-margin + $icon-width + $icon-right-margin; - } - } - - .center { - text-align: center; - } -} From 9c6d836b24c7e084a4e6b0894a448e2065835b7c Mon Sep 17 00:00:00 2001 From: Ally Choung Date: Mon, 10 Feb 2025 14:35:08 -0500 Subject: [PATCH 13/47] fix: remove view encapsulation --- .../table-example/table-example.component.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.ts b/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.ts index baa57d6aa..dc2a7b794 100644 --- a/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.ts +++ b/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.ts @@ -1,10 +1,5 @@ import { CommonModule } from '@angular/common'; -import { - ChangeDetectionStrategy, - Component, - Input, - ViewEncapsulation, -} from '@angular/core'; +import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; import { HsiUiTableDataSource, TableColumn, @@ -24,7 +19,6 @@ enum ColumnNames { templateUrl: './table-example.component.html', styleUrls: ['../../../examples.scss', './table-example.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, - encapsulation: ViewEncapsulation.None, }) export class TableExampleComponent { @Input() sortIcon: string = 'arrow_upward'; From 9f4d19c2e392beaabcb4b6f84c549d3a85b12133 Mon Sep 17 00:00:00 2001 From: Ally Choung Date: Wed, 26 Feb 2025 10:09:50 -0500 Subject: [PATCH 14/47] fix: remove override from disconnect and connect in data source --- libs/ui-components/src/lib/table/table.data-source.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libs/ui-components/src/lib/table/table.data-source.ts b/libs/ui-components/src/lib/table/table.data-source.ts index ac787976f..ecfe7f2f2 100644 --- a/libs/ui-components/src/lib/table/table.data-source.ts +++ b/libs/ui-components/src/lib/table/table.data-source.ts @@ -130,9 +130,9 @@ export class HsiUiTableDataSource extends DataSource { this.sortColId.next(columnId); } - override disconnect(): void {} + disconnect(): void {} - override connect(): Observable { + connect(): Observable { return this.sortedData$; } } From 73d1ef820c2ebebb280a2dc9bd8e1204fa41a350 Mon Sep 17 00:00:00 2001 From: Ally Choung Date: Wed, 26 Feb 2025 10:20:06 -0500 Subject: [PATCH 15/47] fix: rename dataSource sort function --- .../table-example/table-example.component.html | 4 ++-- .../single-sort-header/single-sort-header.component.html | 7 ++++++- libs/ui-components/src/lib/table/table.data-source.spec.ts | 4 ++-- libs/ui-components/src/lib/table/table.data-source.ts | 4 ++-- 4 files changed, 12 insertions(+), 7 deletions(-) diff --git a/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.html b/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.html index d6b470a62..22139c9bc 100644 --- a/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.html +++ b/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.html @@ -1,6 +1,6 @@ @if (columns$ | async; as columns) { - @for (column of columns; track columnTrackingFunction) { + @for (column of columns; track column.id) { @if (column.sortable) { diff --git a/libs/ui-components/src/lib/table/single-sort-header/single-sort-header.component.html b/libs/ui-components/src/lib/table/single-sort-header/single-sort-header.component.html index 75d987622..3b9333cd2 100644 --- a/libs/ui-components/src/lib/table/single-sort-header/single-sort-header.component.html +++ b/libs/ui-components/src/lib/table/single-sort-header/single-sort-header.component.html @@ -1,4 +1,9 @@
{{ column.id }} - {{ sortIcon }} +
diff --git a/libs/ui-components/src/lib/table/table.data-source.spec.ts b/libs/ui-components/src/lib/table/table.data-source.spec.ts index 70dfaedcb..95a13a276 100644 --- a/libs/ui-components/src/lib/table/table.data-source.spec.ts +++ b/libs/ui-components/src/lib/table/table.data-source.spec.ts @@ -72,8 +72,8 @@ describe('DataSource', () => { c: descData, // flips correctly }; const triggerValues = { - x: () => dataSource.handleSort(chosenColumn.id), - y: () => dataSource.handleSort(chosenColumn.id), + x: () => dataSource.sort(chosenColumn), + y: () => dataSource.sort(chosenColumn), }; testScheduler.run(({ expectObservable, cold }) => { expectObservable(connection$).toBe(expectedMarbles, expectedValues); diff --git a/libs/ui-components/src/lib/table/table.data-source.ts b/libs/ui-components/src/lib/table/table.data-source.ts index ecfe7f2f2..501043995 100644 --- a/libs/ui-components/src/lib/table/table.data-source.ts +++ b/libs/ui-components/src/lib/table/table.data-source.ts @@ -126,8 +126,8 @@ export class HsiUiTableDataSource extends DataSource { return columnsWithSortDir; } - handleSort(columnId: string) { - this.sortColId.next(columnId); + sort(column: TableColumn) { + this.sortColId.next(column.id); } disconnect(): void {} From 34b94cbc120e9405605d749bbb900931e10778e1 Mon Sep 17 00:00:00 2001 From: Ally Choung Date: Wed, 26 Feb 2025 11:56:56 -0500 Subject: [PATCH 16/47] fix: add column builder class --- .../table-example/table-example.component.ts | 29 ++-- .../src/lib/table/table-column-builder.ts | 150 ++++++++++++++++++ 2 files changed, 165 insertions(+), 14 deletions(-) create mode 100644 libs/ui-components/src/lib/table/table-column-builder.ts diff --git a/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.ts b/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.ts index dc2a7b794..6c50be794 100644 --- a/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.ts +++ b/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.ts @@ -5,6 +5,7 @@ import { TableColumn, TableModule, } from '@hsi/ui-components'; +import { TableColumnBuilder } from 'libs/ui-components/src/lib/table/table-column-builder'; import { of } from 'rxjs'; enum ColumnNames { @@ -34,20 +35,20 @@ export class TableExampleComponent { ColumnNames = ColumnNames; columns$ = of([ - new TableColumn<{ fruit: string; color: string }>({ - id: ColumnNames.fruit, - ascendingSortFunction: (a, b) => a.fruit.localeCompare(b.fruit), - sortOrder: 1, - sortable: true, - sortDirection: 'asc', // initial sort direction - getFormattedValue: (x) => x.fruit, - }), - new TableColumn<{ fruit: string; color: string }>({ - id: ColumnNames.color, - sortable: true, - ascendingSortFunction: (a, b) => a.color.localeCompare(b.color), - getFormattedValue: (x) => x.color, - }), + new TableColumnBuilder<{ fruit: string; color: string }>() + .id(ColumnNames.fruit) + .ascendingSortFunction((a, b) => a.fruit.localeCompare(b.fruit)) + .sortOrder(1) + .sortable(true) + .sortDirection('asc') // initial sort direction + .getFormattedValue((x) => x.fruit) + ._build('FruitExampleColumn'), + new TableColumnBuilder<{ fruit: string; color: string }>() + .id(ColumnNames.color) + .sortable(true) + .ascendingSortFunction((a, b) => a.color.localeCompare(b.color)) + .getFormattedValue((x) => x.color) + ._build('ColorExampleColumn'), ]); dataSource = new HsiUiTableDataSource(this.data$, this.columns$); columnTrackingFunction( diff --git a/libs/ui-components/src/lib/table/table-column-builder.ts b/libs/ui-components/src/lib/table/table-column-builder.ts new file mode 100644 index 000000000..ca97fcdce --- /dev/null +++ b/libs/ui-components/src/lib/table/table-column-builder.ts @@ -0,0 +1,150 @@ +import { + SortDirection, + SortDirectionType, + TableColumn, + TableValue, +} from './table-column'; + +const DEFAULT = { + _sortable: false, + _sortedOnInit: false, + _sortOrder: Number.MAX_SAFE_INTEGER, + _sortDirection: SortDirection.asc, +}; + +export class TableColumnBuilder { + private _id: string; + private _getSortValue: (x: Datum) => TableValue; + private _getFormattedValue: (x: Datum) => string; + private _ascendingSortFunction: (a: Datum, b: Datum) => number; + private _sortDirection: SortDirectionType; + private _sortable: boolean; + private _activelySorted: boolean; // init false? + private _sortOrder: number; + private _sortedOnInit: boolean; // should this go? + + constructor() { + Object.assign(this, DEFAULT); + } + + /** + * NOT OPTIONAL. A boolean to determine whether the column is sortable. + * + */ + id(id: string): this { + this._id = id; + return this; + } + + /** + * OPTIONAL. A function to extract the value to be sorted on from the datum. + * If not provided, the formatted value will be used for sorting. + * + * To unset, call with `null`. + */ + getSortValue(getSortValue: null): this; + getSortValue(getSortValue: (x: Datum) => TableValue): this; + getSortValue(getSortValue: (x: Datum) => TableValue | null): this { + if (getSortValue === null) { + this._getSortValue = undefined; + return this; + } + this._getSortValue = getSortValue; + return this; + } + + /** + * OPTIONAL. A function to format the value for display in the table. + * + * To unset, call with `null`. + */ + getFormattedValue(getFormattedValue: null): this; + getFormattedValue(getFormattedValue: (x: Datum) => string): this; + getFormattedValue(getFormattedValue: (x: Datum) => string | null): this { + if (getFormattedValue === null) { + this._getFormattedValue = undefined; + return this; + } + this._getFormattedValue = getFormattedValue; + return this; + } + + /** + * OPTIONAL. A function to determine the sort order of the column. + * If not provided, sort with use d3.ascending on the getSortValue or getFormattedValue. + * + * To unset, call with `null`. + */ + ascendingSortFunction(ascendingSortFunction: null): this; + ascendingSortFunction( + ascendingSortFunction: (a: Datum, b: Datum) => number + ): this; + ascendingSortFunction( + ascendingSortFunction: (a: Datum, b: Datum) => number | null + ): this { + if (ascendingSortFunction === null) { + this._ascendingSortFunction = undefined; + return this; + } + this._ascendingSortFunction = ascendingSortFunction; + return this; + } + + /** + * OPTIONAL. The direction to start sorting this column in. + * + * @default SortDirection.asc + */ + sortDirection(sortDirection: SortDirectionType): this { + this._sortDirection = sortDirection; + return this; + } + + /** + * OPTIONAL. A boolean to determine whether the column is sortable. + * + * @default false + */ + sortable(sortable: boolean): this { + this._sortable = sortable; + return this; + } + + /** + * OPTIONAL. A number representing the sort order of the column. + * + * @default Number.MAX_SAFE_INTEGER + */ + sortOrder(sortOrder: number): this { + this._sortOrder = sortOrder; + return this; + } + + /** + * @internal This method is not intended to be used by consumers of this library. + * + * @param columnName A user-intelligible name for the column being built. Used for error messages. Should be title cased. + */ + _build(columnName: string): TableColumn { + this.validateTableColumn(columnName); + return new TableColumn({ + id: this._id, + getSortValue: this._getSortValue, + getFormattedValue: this._getFormattedValue, + ascendingSortFunction: this._ascendingSortFunction, + sortDirection: this._sortDirection, + sortable: this._sortable, + // activelySorted: this._activelySorted, // todo + sortOrder: this._sortOrder, + // sortedOnInit: this._sortedOnInit, + }); + } + + protected validateTableColumn(columnName: string): void { + if (!this._id) { + throw new Error( + `${columnName} Column: id is required. Please use method 'id' to set it.` + ); + } + } +} From 89e000b0689a936b6dff446368358236b86405cf Mon Sep 17 00:00:00 2001 From: Ally Choung Date: Wed, 12 Mar 2025 11:55:24 -0400 Subject: [PATCH 17/47] fix: add internal id to table column --- .../table-example.component.html | 8 +- .../table-example/table-example.component.ts | 89 +++++++++++-------- .../src/assets/ui-components/content/table.md | 69 ++++++++++++++ .../single-sort-header.component.html | 2 +- .../src/lib/table/table-column.ts | 20 ++++- .../src/lib/table/table.data-source.ts | 1 - 6 files changed, 144 insertions(+), 45 deletions(-) diff --git a/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.html b/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.html index 22139c9bc..57178e048 100644 --- a/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.html +++ b/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.html @@ -12,11 +12,11 @@ [sortIcon]="sortIcon" (click)="dataSource.sort(column)" > - {{ column.id }} } @else { } } - - + +
{{ column.id }} - {{ column.id }} + {{ column.label }} @@ -24,7 +24,7 @@
} diff --git a/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.ts b/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.ts index 6c50be794..8f92c7cbf 100644 --- a/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.ts +++ b/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.ts @@ -1,12 +1,18 @@ import { CommonModule } from '@angular/common'; -import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; +import { + ChangeDetectionStrategy, + Component, + DestroyRef, + Input, + OnInit, +} from '@angular/core'; import { HsiUiTableDataSource, TableColumn, TableModule, } from '@hsi/ui-components'; import { TableColumnBuilder } from 'libs/ui-components/src/lib/table/table-column-builder'; -import { of } from 'rxjs'; +import { map, Observable, of, shareReplay } from 'rxjs'; enum ColumnNames { fruit = 'fruit', @@ -21,40 +27,53 @@ enum ColumnNames { styleUrls: ['../../../examples.scss', './table-example.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, }) -export class TableExampleComponent { +export class TableExampleComponent implements OnInit { @Input() sortIcon: string = 'arrow_upward'; - columnIds = [ColumnNames.fruit, ColumnNames.color]; - data$ = of([ - { fruit: 'lemon', color: 'yellow' }, - { fruit: 'mango', color: 'orange' }, - { fruit: 'avocado', color: 'green' }, - { fruit: 'apple', color: 'red' }, - { fruit: 'orange', color: 'orange' }, - { fruit: 'banana', color: 'yellow' }, - ]); - ColumnNames = ColumnNames; + data$: Observable<{ fruit: string; color: string }[]>; + columns$: Observable[]>; + dataSource: HsiUiTableDataSource<{ fruit: string; color: string }>; + tableHeaders$: Observable; + constructor(private destroyRef: DestroyRef) {} + + ngOnInit(): void { + this.setTableData(); + this.setDataSource(); + this.setTableHeaders(); + } - columns$ = of([ - new TableColumnBuilder<{ fruit: string; color: string }>() - .id(ColumnNames.fruit) - .ascendingSortFunction((a, b) => a.fruit.localeCompare(b.fruit)) - .sortOrder(1) - .sortable(true) - .sortDirection('asc') // initial sort direction - .getFormattedValue((x) => x.fruit) - ._build('FruitExampleColumn'), - new TableColumnBuilder<{ fruit: string; color: string }>() - .id(ColumnNames.color) - .sortable(true) - .ascendingSortFunction((a, b) => a.color.localeCompare(b.color)) - .getFormattedValue((x) => x.color) - ._build('ColorExampleColumn'), - ]); - dataSource = new HsiUiTableDataSource(this.data$, this.columns$); - columnTrackingFunction( - _: number, - column: TableColumn<{ fruit: string; color: string }> - ) { - return column.id; + setTableData() { + this.data$ = of([ + { fruit: 'lemon', color: 'yellow' }, + { fruit: 'mango', color: 'orange' }, + { fruit: 'avocado', color: 'green' }, + { fruit: 'apple', color: 'red' }, + { fruit: 'orange', color: 'orange' }, + { fruit: 'banana', color: 'yellow' }, + ]); + this.columns$ = of([ + new TableColumnBuilder<{ fruit: string; color: string }>() + .label(ColumnNames.fruit) + .ascendingSortFunction((a, b) => a.fruit.localeCompare(b.fruit)) + .sortOrder(1) + .sortable(true) + .sortDirection('asc') // initial sort direction + .getFormattedValue((x) => x.fruit) + .getColumn('FruitColumn'), + new TableColumnBuilder<{ fruit: string; color: string }>() + .label(ColumnNames.color) + .sortable(true) + .ascendingSortFunction((a, b) => a.color.localeCompare(b.color)) + .getFormattedValue((x) => x.color) + .getColumn('ColorColumn'), + ]); + } + setDataSource() { + this.dataSource = new HsiUiTableDataSource(this.data$, this.columns$); + } + setTableHeaders(): void { + this.tableHeaders$ = this.columns$.pipe( + map((columns) => columns.map((x) => x.id)), + shareReplay(1) + ); } } diff --git a/apps/demo-app/src/assets/ui-components/content/table.md b/apps/demo-app/src/assets/ui-components/content/table.md index d081b077d..d025bac1b 100644 --- a/apps/demo-app/src/assets/ui-components/content/table.md +++ b/apps/demo-app/src/assets/ui-components/content/table.md @@ -1,5 +1,74 @@ # Table Component +HSI UI Components provides a set of Angular components and configuration classes that can be used to +create tables. These components can be imported via the `TableModule`. + +The HSI UI Components table uses the +[Angular Material CDK Table](https://material.angular.io/cdk/table/overview) as a reference. + +## Composing a Table + +A table is minimally composed of the following HTML components: + +- `table` — A component that is an outer wrapper for other components in the table. +- `th` — A component that contains ... +- `td` — A component that is hidden until the user interacts with the textbox. Contains the + options that the user can select. +- `tr` — A component that creates an option in the listbox. + +In addition, the table must also be given data through an `HsiUiTableDataSource` instance. + +The following is a minimal implementation: + ```custom-angular simple table ``` + +```html +@if (columns$ | async; as columns) { + + @for (column of columns; track column.id) { + + @if (column.sortable) { + + } @else { + + } + + + } + + +
+ {{ column.id }} {{ column.id }} {{ column.getFormattedValue(element) }}
+} +``` + +```ts +TODO insert here +``` + +## Using `HsiUiTableDataSource` + +## `TableColumn` + +# Examples + +## Example using custom icons in table header + +```custom-angular +custom sort icon table +``` + +## Example using pipes for formatting table data + +## More complex sorting examples diff --git a/libs/ui-components/src/lib/table/single-sort-header/single-sort-header.component.html b/libs/ui-components/src/lib/table/single-sort-header/single-sort-header.component.html index 3b9333cd2..d415661a6 100644 --- a/libs/ui-components/src/lib/table/single-sort-header/single-sort-header.component.html +++ b/libs/ui-components/src/lib/table/single-sort-header/single-sort-header.component.html @@ -1,5 +1,5 @@
- {{ column.id }} + {{ column.label }}
} @else { {{ column.label }} } - + {{ element | getValueByKey: column.key }} diff --git a/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.scss b/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.scss index 52a25becb..381f55b6b 100644 --- a/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.scss +++ b/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.scss @@ -8,6 +8,7 @@ $icon-right-margin: 0.4rem; .header-cell-sort { display: flex; align-items: flex-end; + justify-content: flex-end; &:hover { cursor: pointer; } @@ -37,29 +38,16 @@ $icon-right-margin: 0.4rem; transform: rotate(180deg); } -.left { - text-align: left; -} - -.right { +.table-cell { text-align: right; - .header-cell-sort { - justify-content: flex-end; - } - &.sorted-cell { padding-right: $icon-left-margin + $icon-width + $icon-right-margin; } } -.center { - text-align: center; -} - .table-container { border-spacing: 0; - td:last-child { &.left { padding-right: 0; From 2fb5fae4579f407050bd59be096fee04f43629f2 Mon Sep 17 00:00:00 2001 From: Ally Choung Date: Fri, 4 Apr 2025 12:14:43 -0400 Subject: [PATCH 30/47] fix: view encapsulation for builder method docs styling in table content --- .../table-content/table-content.component.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/apps/demo-app/src/app/content/ui-components/table-content/table-content.component.ts b/apps/demo-app/src/app/content/ui-components/table-content/table-content.component.ts index b3676ea41..909617a2c 100644 --- a/apps/demo-app/src/app/content/ui-components/table-content/table-content.component.ts +++ b/apps/demo-app/src/app/content/ui-components/table-content/table-content.component.ts @@ -1,5 +1,9 @@ import { CommonModule } from '@angular/common'; -import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { + ChangeDetectionStrategy, + Component, + ViewEncapsulation, +} from '@angular/core'; import { SinglePanelExampleDisplayComponent } from '../../../platform/single-panel-example-display/single-panel-example-display.component'; import { ContentContainerComponent } from '../../content-container/content-container.component'; import { TableExampleComponent } from './table-example/table-example.component'; @@ -14,7 +18,12 @@ import { TableExampleComponent } from './table-example/table-example.component'; ContentContainerComponent, ], templateUrl: './table-content.component.html', - styleUrls: ['../../examples.scss', './table-content.component.scss'], + styleUrls: [ + '../../examples.scss', + '../../api-documentation.scss', + './table-content.component.scss', + ], changeDetection: ChangeDetectionStrategy.OnPush, + encapsulation: ViewEncapsulation.None, }) export class TableContentComponent {} From f1e08833738157b1d182ad1ff6a3725d51ed61d6 Mon Sep 17 00:00:00 2001 From: Ally Choung Date: Fri, 4 Apr 2025 16:25:24 -0400 Subject: [PATCH 31/47] fix: update docs for both table column builders --- .../src/lib/table/table-column-builder.ts | 41 ++++++++++++++----- .../src/lib/table/table-columns.builder.ts | 10 +++-- 2 files changed, 36 insertions(+), 15 deletions(-) diff --git a/libs/ui-components/src/lib/table/table-column-builder.ts b/libs/ui-components/src/lib/table/table-column-builder.ts index cac26d15e..425641c1f 100644 --- a/libs/ui-components/src/lib/table/table-column-builder.ts +++ b/libs/ui-components/src/lib/table/table-column-builder.ts @@ -30,14 +30,18 @@ export class TableColumnBuilder { } /** - * REQUIRED. A string to use as the id of the table column. + * REQUIRED. Determines the id of the table column. + * + * @param id A string to use as the id of the table column. */ id(id: string): this { this._id = id; return this; } /** - * OPTIONAL. A string to use as the label of the table column. + * OPTIONAL. Determines the label of the table column. + * + * @param label A string to use as the label of the table column. */ label(label: string): this { this._label = label; @@ -45,7 +49,11 @@ export class TableColumnBuilder { } /** - * REQUIRED. A string to use as the key of the table column. + * REQUIRED. Determines the property key in the datum that + * is to be displayed in this table column. + * + * @param key A string to use as the display key of the table column. Nested object data + * can be accessed using dot notation (e.g. `user.name`). */ displayKey(key: string): this { this._key = key; @@ -53,9 +61,11 @@ export class TableColumnBuilder { } /** - * OPTIONAL. A function to extract the value to be sorted on from the datum. + * OPTIONAL. Specifies how to extract the datum property to be sorted + * on for cells in this table column. * - * To unset, call with `null`. + * @param getSortValue A function to extract the datum property to be + * sorted on for cells in this table column, or `null` to not set this property. */ getSortValue(getSortValue: null): this; getSortValue(getSortValue: (x: Datum) => TableValue): this; @@ -69,10 +79,11 @@ export class TableColumnBuilder { } /** - * OPTIONAL. A function to determine the sort order of the column. - * If not provided, sort with use d3.ascending on the getSortValue. + * OPTIONAL. Specifies how datum are to be sorted in ascending order for this table column. + * If not provided, this column will use `d3.ascending` on the getSortValue. * - * To unset, call with `null`. + * @param ascendingSortFunction A function to sort datum in ascending order for this table column, + * or `null` to not set this property. */ ascendingSortFunction(ascendingSortFunction: null): this; ascendingSortFunction( @@ -90,8 +101,9 @@ export class TableColumnBuilder { } /** - * OPTIONAL. The direction to start sorting this column in. + * OPTIONAL. Determines the direction to start sorting this column in. * + * @param sortDirection A SortDirectionType to use as the sort direction of the table column. * @default SortDirection.asc */ sortDirection(sortDirection: SortDirectionType): this { @@ -100,8 +112,9 @@ export class TableColumnBuilder { } /** - * OPTIONAL. A boolean to determine whether the column is sortable. + * OPTIONAL. Determines whether the column is sortable. * + * @param sortable A boolean to use as the sortable property of the table column. * @default false */ sortable(sortable: boolean): this { @@ -110,8 +123,9 @@ export class TableColumnBuilder { } /** - * OPTIONAL. A number representing the sort order of the column. + * OPTIONAL. Determines the sort order of the table column. * + * @param sortOrder A number to use as the sort order of the table column. * @default Number.MAX_SAFE_INTEGER */ sortOrder(sortOrder: number): this { @@ -138,6 +152,11 @@ export class TableColumnBuilder { }); } + /** + * Validates the table column properties before initializing the TableColumn instance. + * + * @param columnName A user-intelligible name for the column being built. Used for error messages. Should be title cased. + */ protected validateTableColumn(columnName: string): void { if (!this._id || !this._key) { throw new Error(`ColumnBuilder: ${columnName}. ID and key are required.`); diff --git a/libs/ui-components/src/lib/table/table-columns.builder.ts b/libs/ui-components/src/lib/table/table-columns.builder.ts index 115d72cd5..5152b49db 100644 --- a/libs/ui-components/src/lib/table/table-columns.builder.ts +++ b/libs/ui-components/src/lib/table/table-columns.builder.ts @@ -8,8 +8,9 @@ export class TableColumnsBuilder { private _columnBuilders: TableColumnBuilder[] = []; /** - * Adds a new column to the list of columns. - * @param columnBuilder The builder for the column to add. + * REQUIRED. Specifies a new column to be added to the end of the list of columns. + * + * @param columnBuilder The builder for the table column to add. */ addColumn( applyToColumn: ( @@ -21,8 +22,9 @@ export class TableColumnsBuilder { } /** - * Builds the list of columns. - * @returns The list of columns built by the builder. + * REQUIRED. Validates and builds the configuration object for the table columns that can be passed to HsiUiTableDataSource. + * + * The user must call this at the end of the chain of methods to build the configuration object. */ getConfig(): TableColumn[] { this.validateColumns(); From 2981fedb353b5e4a9cda98a0ebb173099e81d14d Mon Sep 17 00:00:00 2001 From: Ally Choung Date: Mon, 7 Apr 2025 18:11:30 -0400 Subject: [PATCH 32/47] fix: update table builder docs --- .../src/assets/ui-components/content/table.md | 233 ++++++++++++------ .../src/lib/table/table-column-builder.ts | 14 +- .../src/lib/table/table-columns.builder.ts | 2 +- 3 files changed, 172 insertions(+), 77 deletions(-) diff --git a/apps/demo-app/src/assets/ui-components/content/table.md b/apps/demo-app/src/assets/ui-components/content/table.md index 1abb09098..05bbf5c88 100644 --- a/apps/demo-app/src/assets/ui-components/content/table.md +++ b/apps/demo-app/src/assets/ui-components/content/table.md @@ -147,22 +147,26 @@ export class TableExampleComponent implements OnInit { *cdkHeaderCellDef="let element" (click)="dataSource.sort(column)" > - {{ sortIcon }} - {{ column.label }} + {{ column.label }} + {{ sortIcon }} + } @else { {{ column.label }} } - {{ element | getValueByKey: column.key }} + + {{ element | getValueByKey: column.key }} + } @@ -242,62 +246,139 @@ how to sort the column. The following methods can be called on `TableColumnsBuilder` to create a list of validated `TableColumn`s. -- `addColumn((column: TableColumnBuilder) => TableColumnBuilder)` - adds a column to this builder's - list of table columns -- `getConfig()` - builds the list of table columns +```builder-method +name: addColumn +description: 'Specifies a new column to be added to the end of the list of columns.' +params: + - name: applyToColumn + type: '(column: TableColumnBuilder) => TableColumnBuilder' + description: + - 'Table column configuration for the column to be added, with all desired builder methods applied. See required methods below in the `TableColumnBuilder` section.' +``` + +```builder-method +name: getConfig +description: 'Validates and builds the configuration object for the table columns that can be passed to `HsiUiTableDataSource`.' +params: +- '' +``` ### `TableColumnBuilder` Methods #### Required Methods -- `id(id: string)` - sets the id of the table column -- `displayKey(key: string)` - sets the key of the table column. This is used for accessing the data - within the given `Datum`. A key should be formatted like a path to the desired data in the object. - See how the `metrics.cost` subkey is set in the example below. - - ```ts - // Datum - fruits = [{ - fruit: 'apple', - color: 'green', - metrics: { - cost: 21, - quantity: 2 - }, - ... - }] +```builder-method +name: id +description: 'Sets the id of the table column.' +params: + - name: id + type: string + description: + - 'The assigned id of the table column.' +``` - // Builder - builder = new TableColumnsBuilder<{ - fruit: string; - color: string; +```builder-method +name: displayKey +description: 'Sets the property key in the datum that is to be displayed in the table column. See the `metrics.cost` display key is set in the example below.' +params: + - name: key + type: string + description: + - 'The display key of the table column. Nested object data can be accessed using dot notation (e.g. `metrics.cost`, `metrics.inventory`).' +``` + +```ts +// Datum +fruits = [{ + fruit: 'apple', + color: 'green', metrics: { - inventory: number; - price: number; - } - }>() + cost: 21, + quantity: 2 + }, ... - .addColumn( - (column) => - column - .label('Cost') - .displayKey('metrics.cost') - ) - ... - ``` +}] + +// Builder +builder = new TableColumnsBuilder<{ +fruit: string; +color: string; +metrics: { + inventory: number; + price: number; +} +}>() +... + .addColumn( + (column) => + column + .label('Cost') + .displayKey('metrics.cost') + ) + ... +``` #### Optional Methods -- `label(label: string)` - sets the label of the table column -- `getSortValue(getSortValue: (x: Datum) => TableValue)` - sets the function that extracts the value - to be sorted on from the datum -- `ascendingSortFunction((a: Datum, b: Datum) => number)` - sets the function by which to sort the - values in this column -- `sortDirection(sortDirection: SortDirectionType)` - sets the starting direction by which to sort - this column -- `sortable(sortable: boolean)` - sets whether or not this column can be sorted -- `sortOrder(sortOrder: number)` - sets the order in which this column is to be sorted by in the - case of tiebreaks +```builder-method +name: label +description: 'Sets the label of the table column. Useful for storing the desired header text for the column.' +params: + - name: label + type: string + description: + - 'The label to be used by the table column' +``` + +```builder-method +name: getSortValue +description: 'Specifies how to extract the datum property to be sorted on for cells in the table column.' +params: + - name: getSortValue + type: '(x: Datum) => TableValue | null' + description: + - 'A function to extract the datum property to be sorted on for cells in the table column, or `null` to not set this property.' +``` + +```builder-method +name: ascendingSortFunction +description: 'Specifies how datum are to be sorted in ascending order for the table column. If not provided, the column will use `d3.ascending` on the getSortValue output.' +params: + - name: ascendingSortFunction + type: '(a: Datum, b: Datum) => number | null' + description: + - 'The function that sorts datum in ascending order for the table column.' +``` + +```builder-method +name: sortDirection +description: Sets the direction the table column is sorted in. +params: + - name: sortDirection + type: SortDirectionType + description: + - 'The sort direction of the table column.' +``` + +```builder-method +name: sortable +description: 'Sets whether or not the table column can be sorted.' +params: + - name: sortable + type: boolean + description: + - 'Whether the column can be sorted.' +``` + +```builder-method +name: sortOrder +description: 'Sets the order in which the table column is to be sorted by in the case of tiebreaks' +params: + - name: sortOrder + type: number + description: + - 'The sort order of the table column.' +``` ### Accessing data values using `GetValueByKeyPipe` @@ -351,9 +432,12 @@ In the `th` element: > ``` -Example CSS code for styling icons: +Example CSS code for styling icons in a table: ```scss +@use 'vars' as *; +@use 'functions' as *; +@use 'colors'; $icon-width: 0.9rem; $icon-left-margin: 0.2rem; $icon-right-margin: 0.4rem; @@ -361,6 +445,7 @@ $icon-right-margin: 0.4rem; .header-cell-sort { display: flex; align-items: flex-end; + justify-content: flex-end; &:hover { cursor: pointer; } @@ -390,23 +475,33 @@ $icon-right-margin: 0.4rem; transform: rotate(180deg); } -.left { - text-align: left; -} - -.right { +.table-cell { text-align: right; - .header-cell-sort { - justify-content: flex-end; - } - &.sorted-cell { padding-right: $icon-left-margin + $icon-width + $icon-right-margin; } } -.center { - text-align: center; +.table-container { + border-spacing: 0; + td:last-child { + &.left { + padding-right: 0; + } + } + + th:last-child { + &.left { + padding-right: 0; + } + } + + th { + vertical-align: bottom; + &.sorted-header { + padding-right: 0; + } + } } ``` diff --git a/libs/ui-components/src/lib/table/table-column-builder.ts b/libs/ui-components/src/lib/table/table-column-builder.ts index 425641c1f..d2502e083 100644 --- a/libs/ui-components/src/lib/table/table-column-builder.ts +++ b/libs/ui-components/src/lib/table/table-column-builder.ts @@ -50,7 +50,7 @@ export class TableColumnBuilder { /** * REQUIRED. Determines the property key in the datum that - * is to be displayed in this table column. + * is to be displayed in the table column. * * @param key A string to use as the display key of the table column. Nested object data * can be accessed using dot notation (e.g. `user.name`). @@ -62,10 +62,10 @@ export class TableColumnBuilder { /** * OPTIONAL. Specifies how to extract the datum property to be sorted - * on for cells in this table column. + * on for cells in the table column. * * @param getSortValue A function to extract the datum property to be - * sorted on for cells in this table column, or `null` to not set this property. + * sorted on for cells in the table column, or `null` to not set this property. */ getSortValue(getSortValue: null): this; getSortValue(getSortValue: (x: Datum) => TableValue): this; @@ -79,10 +79,10 @@ export class TableColumnBuilder { } /** - * OPTIONAL. Specifies how datum are to be sorted in ascending order for this table column. - * If not provided, this column will use `d3.ascending` on the getSortValue. + * OPTIONAL. Specifies how datum are to be sorted in ascending order for the table column. + * If not provided, the column will use `d3.ascending` on the getSortValue output. * - * @param ascendingSortFunction A function to sort datum in ascending order for this table column, + * @param ascendingSortFunction A function to sort datum in ascending order for the table column, * or `null` to not set this property. */ ascendingSortFunction(ascendingSortFunction: null): this; @@ -101,7 +101,7 @@ export class TableColumnBuilder { } /** - * OPTIONAL. Determines the direction to start sorting this column in. + * OPTIONAL. Determines the direction to start sorting the column in. * * @param sortDirection A SortDirectionType to use as the sort direction of the table column. * @default SortDirection.asc diff --git a/libs/ui-components/src/lib/table/table-columns.builder.ts b/libs/ui-components/src/lib/table/table-columns.builder.ts index 5152b49db..88d8a8db7 100644 --- a/libs/ui-components/src/lib/table/table-columns.builder.ts +++ b/libs/ui-components/src/lib/table/table-columns.builder.ts @@ -22,7 +22,7 @@ export class TableColumnsBuilder { } /** - * REQUIRED. Validates and builds the configuration object for the table columns that can be passed to HsiUiTableDataSource. + * REQUIRED. Validates and builds the configuration object for the table columns that can be passed to `HsiUiTableDataSource`. * * The user must call this at the end of the chain of methods to build the configuration object. */ From 21a73b65894a7ac98d936a3009909fb6ca48a9e2 Mon Sep 17 00:00:00 2001 From: Ally Choung Date: Mon, 7 Apr 2025 18:15:35 -0400 Subject: [PATCH 33/47] fix: default in builder doc --- libs/ui-components/src/lib/table/table-column-builder.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/libs/ui-components/src/lib/table/table-column-builder.ts b/libs/ui-components/src/lib/table/table-column-builder.ts index d2502e083..e378520fa 100644 --- a/libs/ui-components/src/lib/table/table-column-builder.ts +++ b/libs/ui-components/src/lib/table/table-column-builder.ts @@ -104,7 +104,7 @@ export class TableColumnBuilder { * OPTIONAL. Determines the direction to start sorting the column in. * * @param sortDirection A SortDirectionType to use as the sort direction of the table column. - * @default SortDirection.asc + * If not called, the default value is `SortDirection.asc`. */ sortDirection(sortDirection: SortDirectionType): this { this._sortDirection = sortDirection; @@ -115,7 +115,7 @@ export class TableColumnBuilder { * OPTIONAL. Determines whether the column is sortable. * * @param sortable A boolean to use as the sortable property of the table column. - * @default false + * If not called, the default value is `false`. */ sortable(sortable: boolean): this { this._sortable = sortable; @@ -126,7 +126,8 @@ export class TableColumnBuilder { * OPTIONAL. Determines the sort order of the table column. * * @param sortOrder A number to use as the sort order of the table column. - * @default Number.MAX_SAFE_INTEGER + * + * If not called, the default value is `Number.MAX_SAFE_INTEGER`. */ sortOrder(sortOrder: number): this { this._sortOrder = sortOrder; From d01e7d37ca44056b52dcfe9ec2afb0081c2c6d46 Mon Sep 17 00:00:00 2001 From: Ally Choung Date: Tue, 8 Apr 2025 15:05:52 -0400 Subject: [PATCH 34/47] fix: add css class prop to table column --- .../table-example.component.html | 9 ++-- .../table-example.component.scss | 18 ++++++-- .../table-example/table-example.component.ts | 3 ++ .../src/assets/ui-components/content/table.md | 45 ++++++++++++++++--- .../src/lib/table/table-column-builder.ts | 13 ++++++ .../src/lib/table/table-column.ts | 6 ++- 6 files changed, 81 insertions(+), 13 deletions(-) diff --git a/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.html b/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.html index ddd30645d..790fbe195 100644 --- a/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.html +++ b/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.html @@ -9,7 +9,7 @@ *cdkHeaderCellDef="let element" (click)="dataSource.sort(column)" > -
+
{{ column.label }} diff --git a/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.scss b/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.scss index 381f55b6b..5d074679d 100644 --- a/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.scss +++ b/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.scss @@ -7,11 +7,17 @@ $icon-right-margin: 0.4rem; .header-cell-sort { display: flex; - align-items: flex-end; - justify-content: flex-end; &:hover { cursor: pointer; } + &.left { + align-items: flex-start; + justify-content: flex-start; + } + &.right { + align-items: flex-end; + justify-content: flex-end; + } } .material-symbols-outlined { @@ -38,9 +44,15 @@ $icon-right-margin: 0.4rem; transform: rotate(180deg); } -.table-cell { +.right { text-align: right; +} +.left { + text-align: left; +} + +.table-cell { &.sorted-cell { padding-right: $icon-left-margin + $icon-width + $icon-right-margin; } diff --git a/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.ts b/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.ts index f3be51978..623db2072 100644 --- a/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.ts +++ b/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.ts @@ -91,6 +91,7 @@ export class TableExampleComponent implements OnInit { (column) => column .label(ColumnNames.fruit) + .cssClass('left') .displayKey(FruitInfo.fruit) .ascendingSortFunction((a, b) => a.fruit.localeCompare(b.fruit)) .sortOrder(1) @@ -101,6 +102,7 @@ export class TableExampleComponent implements OnInit { column .displayKey(FruitInfo.colorName) .label(ColumnNames.colorName) + .cssClass('left') .sortable(true) .ascendingSortFunction((a, b) => a.color.localeCompare(b.color)) ) @@ -108,6 +110,7 @@ export class TableExampleComponent implements OnInit { column .displayKey(FruitInfo.inventory) .label(ColumnNames.inventory) + .cssClass('right') .sortable(true) .getSortValue((x) => x.metrics.inventory) ) diff --git a/apps/demo-app/src/assets/ui-components/content/table.md b/apps/demo-app/src/assets/ui-components/content/table.md index 05bbf5c88..e674a32b9 100644 --- a/apps/demo-app/src/assets/ui-components/content/table.md +++ b/apps/demo-app/src/assets/ui-components/content/table.md @@ -108,6 +108,7 @@ export class TableExampleComponent implements OnInit { (column) => column .label(ColumnNames.fruit) + .cssClass('left') .displayKey(FruitInfo.fruit) .ascendingSortFunction((a, b) => a.fruit.localeCompare(b.fruit)) .sortOrder(1) @@ -118,6 +119,7 @@ export class TableExampleComponent implements OnInit { column .displayKey(FruitInfo.colorName) .label(ColumnNames.colorName) + .cssClass('left') .sortable(true) .ascendingSortFunction((a, b) => a.color.localeCompare(b.color)) ) @@ -125,6 +127,7 @@ export class TableExampleComponent implements OnInit { column .displayKey(FruitInfo.inventory) .label(ColumnNames.inventory) + .cssClass('right') .sortable(true) .getSortValue((x) => x.metrics.inventory) ) @@ -147,7 +150,7 @@ export class TableExampleComponent implements OnInit { *cdkHeaderCellDef="let element" (click)="dataSource.sort(column)" > -
+
{{ column.label }} {{ column.label }} } - + {{ element | getValueByKey: column.key }} @@ -327,7 +338,17 @@ params: - name: label type: string description: - - 'The label to be used by the table column' + - 'The label to be used by the table column.' +``` + +```builder-method +name: cssClass +description: 'Sets the CSS class of the table column.' +params: + - name: cssClass + type: string + description: + - 'The CSS class to be used by the table column.' ``` ```builder-method @@ -444,11 +465,17 @@ $icon-right-margin: 0.4rem; .header-cell-sort { display: flex; - align-items: flex-end; - justify-content: flex-end; &:hover { cursor: pointer; } + &.left { + align-items: flex-start; + justify-content: flex-start; + } + &.right { + align-items: flex-end; + justify-content: flex-end; + } } .material-symbols-outlined { @@ -475,9 +502,15 @@ $icon-right-margin: 0.4rem; transform: rotate(180deg); } -.table-cell { +.right { text-align: right; +} +.left { + text-align: left; +} + +.table-cell { &.sorted-cell { padding-right: $icon-left-margin + $icon-width + $icon-right-margin; } diff --git a/libs/ui-components/src/lib/table/table-column-builder.ts b/libs/ui-components/src/lib/table/table-column-builder.ts index e378520fa..28cd2a97a 100644 --- a/libs/ui-components/src/lib/table/table-column-builder.ts +++ b/libs/ui-components/src/lib/table/table-column-builder.ts @@ -6,6 +6,7 @@ import { } from './table-column'; const DEFAULT = { + _cssClass: '', _sortable: false, _sortedOnInit: false, _sortOrder: Number.MAX_SAFE_INTEGER, @@ -16,6 +17,7 @@ const DEFAULT = { * An internal builder class for a single table column. */ export class TableColumnBuilder { + private _cssClass: string; private _id: string; private _key: string; private _label: string; @@ -29,6 +31,16 @@ export class TableColumnBuilder { Object.assign(this, DEFAULT); } + /** + * OPTIONAL. Determines additional CSS classes to be applied to the table column. + * + * @param cssClass A string to use as the CSS class of the table column. + */ + cssClass(cssClass: string): this { + this._cssClass = cssClass; + return this; + } + /** * REQUIRED. Determines the id of the table column. * @@ -145,6 +157,7 @@ export class TableColumnBuilder { id: this._id, label: this._label, key: this._key, + cssClass: this._cssClass, getSortValue: this._getSortValue, ascendingSortFunction: this._ascendingSortFunction, sortDirection: this._sortDirection, diff --git a/libs/ui-components/src/lib/table/table-column.ts b/libs/ui-components/src/lib/table/table-column.ts index 2eaeac318..b1c46a235 100644 --- a/libs/ui-components/src/lib/table/table-column.ts +++ b/libs/ui-components/src/lib/table/table-column.ts @@ -9,13 +9,17 @@ export type SortDirectionType = keyof typeof SortDirection; export type TableValue = string | number | boolean | Date; export class TableColumn { + /** + * The CSS class of the column. + * @default '' + */ + cssClass: string = ''; /** * The unique id of the column. * */ readonly id: string; /** * The unique key of the column. Used for sorting and accessing the column data. - * */ readonly key: string; /** From f8ef599bce9b9769dd8f31f19376aa81e5e3a31d Mon Sep 17 00:00:00 2001 From: Ally Choung Date: Wed, 7 May 2025 14:19:46 -0400 Subject: [PATCH 35/47] fix: work --- .../table-content.component.html | 4 +- .../table-content/table-content.component.ts | 4 +- .../tanstack-example.component.html | 59 +++++++ .../tanstack-example.component.scss | 3 + .../tanstack-example.component.ts | 161 ++++++++++++++++++ .../src/assets/ui-components/content/table.md | 6 + package-lock.json | 34 ++++ package.json | 1 + 8 files changed, 268 insertions(+), 4 deletions(-) create mode 100644 apps/demo-app/src/app/content/ui-components/table-content/tanstack-example/tanstack-example.component.html create mode 100644 apps/demo-app/src/app/content/ui-components/table-content/tanstack-example/tanstack-example.component.scss create mode 100644 apps/demo-app/src/app/content/ui-components/table-content/tanstack-example/tanstack-example.component.ts diff --git a/apps/demo-app/src/app/content/ui-components/table-content/table-content.component.html b/apps/demo-app/src/app/content/ui-components/table-content/table-content.component.html index 1d74f0703..8834c0241 100644 --- a/apps/demo-app/src/app/content/ui-components/table-content/table-content.component.html +++ b/apps/demo-app/src/app/content/ui-components/table-content/table-content.component.html @@ -4,11 +4,11 @@ @case ('simple table') { - + } } diff --git a/apps/demo-app/src/app/content/ui-components/table-content/table-content.component.ts b/apps/demo-app/src/app/content/ui-components/table-content/table-content.component.ts index 2e8c97de2..ddf53d89b 100644 --- a/apps/demo-app/src/app/content/ui-components/table-content/table-content.component.ts +++ b/apps/demo-app/src/app/content/ui-components/table-content/table-content.component.ts @@ -6,14 +6,14 @@ import { } from '@angular/core'; import { SinglePanelExampleDisplayComponent } from '../../../platform/single-panel-example-display/single-panel-example-display.component'; import { ContentContainerComponent } from '../../content-container/content-container.component'; -import { TableExampleComponent } from './table-example/table-example.component'; +import { TanstackExampleComponent } from './tanstack-example/tanstack-example.component'; @Component({ selector: 'app-table-content', imports: [ CommonModule, SinglePanelExampleDisplayComponent, - TableExampleComponent, + TanstackExampleComponent, ContentContainerComponent, ], templateUrl: './table-content.component.html', diff --git a/apps/demo-app/src/app/content/ui-components/table-content/tanstack-example/tanstack-example.component.html b/apps/demo-app/src/app/content/ui-components/table-content/tanstack-example/tanstack-example.component.html new file mode 100644 index 000000000..ffa5983dc --- /dev/null +++ b/apps/demo-app/src/app/content/ui-components/table-content/tanstack-example/tanstack-example.component.html @@ -0,0 +1,59 @@ +
+ + + @for (headerGroup of table.getHeaderGroups(); track headerGroup.id) { + + @for (header of headerGroup.headers; track header.id) { + @if (!header.isPlaceholder) { + @if (header.column.getCanSort()) { + + } @else { + + } + } + } + + } + + + + + + +
+
+ {{ header }} +
+
+ +
+
+
+ + + + + {{ cell.getValue() }} + +
+
diff --git a/apps/demo-app/src/app/content/ui-components/table-content/tanstack-example/tanstack-example.component.scss b/apps/demo-app/src/app/content/ui-components/table-content/tanstack-example/tanstack-example.component.scss new file mode 100644 index 000000000..76ff8ba14 --- /dev/null +++ b/apps/demo-app/src/app/content/ui-components/table-content/tanstack-example/tanstack-example.component.scss @@ -0,0 +1,3 @@ +.sorted { + background-color: green; +} diff --git a/apps/demo-app/src/app/content/ui-components/table-content/tanstack-example/tanstack-example.component.ts b/apps/demo-app/src/app/content/ui-components/table-content/tanstack-example/tanstack-example.component.ts new file mode 100644 index 000000000..1293e2b6c --- /dev/null +++ b/apps/demo-app/src/app/content/ui-components/table-content/tanstack-example/tanstack-example.component.ts @@ -0,0 +1,161 @@ +import { CommonModule } from '@angular/common'; +import { ChangeDetectionStrategy, Component, signal } from '@angular/core'; +import { toObservable, toSignal } from '@angular/core/rxjs-interop'; +import { + ColumnDef, + createAngularTable, + FlexRenderDirective, + getCoreRowModel, + SortingState, +} from '@tanstack/angular-table'; +import { combineLatest, map, of } from 'rxjs'; +import { BarsSimpleStatesExampleComponent } from '../../../viz-components/bars-content/bars-simple-states-example/bars-simple-states-example.component'; + +type Person = { + firstName: string; + lastName: string; + age: number; + performance: { + visits: number; + status: string; + progress: number; + }; +}; + +const defaultData: Person[] = [ + { + firstName: 'tanner', + lastName: 'linsley', + age: 24, + performance: { + visits: 100, + status: 'In Relationship', + progress: 50, + }, + }, + { + firstName: 'joe', + lastName: 'dirte', + age: 45, + performance: { + visits: 20, + status: 'Single', + progress: 10, + }, + }, + { + firstName: 'tandy', + lastName: 'miller', + age: 40, + performance: { + visits: 40, + status: 'Single', + progress: 80, + }, + }, +]; + +const defaultColumns: ColumnDef[] = [ + { + accessorKey: 'firstName', + cell: (info) => info.getValue(), + footer: (info) => info.column.id, + }, + { + accessorFn: (row) => row.lastName, + id: 'lastName', + cell: (info) => `${info.getValue()}`, + header: () => `Last Name`, + footer: (info) => info.column.id, + }, + { + accessorKey: 'age', + header: () => 'Age', + footer: (info) => info.column.id, + sortingFn: 'basic', + enableSorting: true, + }, + { + accessorKey: 'performance.visits', + header: () => `Visits`, + footer: (info) => info.column.id, + sortingFn: (a, b) => { + const aValue = a.getValue('performance.visits'); + const bValue = b.getValue('performance.visits'); + return aValue - bValue; + }, + }, + { + accessorKey: 'performance.status', + header: 'Status', + footer: (info) => info.column.id, + sortingFn: 'basic', + }, + { + accessorKey: 'performance.progress', + header: () => `Progress`, + footer: (info) => info.column.id, + }, +]; + +@Component({ + selector: 'app-tanstack-example', + standalone: true, + imports: [ + FlexRenderDirective, + CommonModule, + BarsSimpleStatesExampleComponent, + ], + templateUrl: './tanstack-example.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + styleUrls: ['./tanstack-example.component.scss'], +}) +export class TanstackExampleComponent { + onClick(id: string) { + console.log('onClick', id); + } + readonly sorting = signal([ + { + id: 'age', + desc: false, //sort by age in descending order by default + }, + ]); + + //Use our controlled state values to fetch data + readonly data$ = combineLatest({ + data: of(defaultData), += sorting: toObservable(this.sorting), + }).pipe(map(({ data, sorting }) => updateData(data, sorting))); + + readonly data = toSignal(this.data$); + table = createAngularTable(() => ({ + data: this.data(), + columns: defaultColumns, + getCoreRowModel: getCoreRowModel(), + initialState: { + sorting: this.sorting(), + }, + enableSorting: true, + manualSorting: true, + onSortingChange: (updater) => { + if (updater instanceof Function) { + this.sorting.update(updater); + } else { + this.sorting.set(updater); + } + }, + })); +} +function updateData(data: Person[], sorting: SortingState): Person[] { + if (sorting.length === 0) return data; + + const { id, desc } = sorting[0]; + return data.sort((a, b) => { + const aValue = id.split('.').reduce((obj, key) => obj[key], a); + const bValue = id.split('.').reduce((obj, key) => obj[key], b); + + if (aValue < bValue) return desc ? 1 : -1; + if (aValue > bValue) return desc ? -1 : 1; + return 0; + }); +} diff --git a/apps/demo-app/src/assets/ui-components/content/table.md b/apps/demo-app/src/assets/ui-components/content/table.md index e674a32b9..ad62c5f8b 100644 --- a/apps/demo-app/src/assets/ui-components/content/table.md +++ b/apps/demo-app/src/assets/ui-components/content/table.md @@ -1,5 +1,11 @@ # Table Component +## TANSTACK EXAMPLE + +```custom-angular +simple table +``` + HSI UI Components provides a set of Angular components and configuration classes that can be used to create tables. These components can be imported via the `TableModule`. diff --git a/package-lock.json b/package-lock.json index 1fd76def2..968bcecb4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "@angular/platform-browser": "19.2.2", "@angular/platform-browser-dynamic": "19.2.2", "@angular/router": "19.2.2", + "@tanstack/angular-table": "^8.21.3", "copyfiles": "^2.4.1", "csstype": "^3.1.3", "d3": "^7.8.5", @@ -13329,6 +13330,39 @@ "node": ">=10" } }, + "node_modules/@tanstack/angular-table": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/@tanstack/angular-table/-/angular-table-8.21.3.tgz", + "integrity": "sha512-8VqEGObnTBNJm3qQSPy+WEGqjXDLesEPSnBTEHdbHFh1rMP1pQsgI85Dwy8OX4kB82wIDvJuw4LceGIXZEbotA==", + "license": "MIT", + "dependencies": { + "@tanstack/table-core": "8.21.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "@angular/core": ">=17" + } + }, + "node_modules/@tanstack/table-core": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.21.3.tgz", + "integrity": "sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/@thednp/event-listener": { "version": "2.0.8", "resolved": "https://registry.npmjs.org/@thednp/event-listener/-/event-listener-2.0.8.tgz", diff --git a/package.json b/package.json index 46ab947df..c8688de2d 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,7 @@ "@angular/platform-browser": "19.2.2", "@angular/platform-browser-dynamic": "19.2.2", "@angular/router": "19.2.2", + "@tanstack/angular-table": "^8.21.3", "copyfiles": "^2.4.1", "csstype": "^3.1.3", "d3": "^7.8.5", From 13044ade52175c93c4328d8e5253252fd4f8f5bf Mon Sep 17 00:00:00 2001 From: tcoile Date: Wed, 7 May 2025 11:54:17 -0700 Subject: [PATCH 36/47] fix: get basic sort to work without actually understanding anything --- .../tanstack-example.component.html | 26 ++++++--- .../tanstack-example.component.ts | 58 ++++++++++--------- 2 files changed, 48 insertions(+), 36 deletions(-) diff --git a/apps/demo-app/src/app/content/ui-components/table-content/tanstack-example/tanstack-example.component.html b/apps/demo-app/src/app/content/ui-components/table-content/tanstack-example/tanstack-example.component.html index ffa5983dc..472a3429f 100644 --- a/apps/demo-app/src/app/content/ui-components/table-content/tanstack-example/tanstack-example.component.html +++ b/apps/demo-app/src/app/content/ui-components/table-content/tanstack-example/tanstack-example.component.html @@ -7,16 +7,26 @@ @if (!header.isPlaceholder) { @if (header.column.getCanSort()) { -
- {{ header }} -
+
+ {{ headerCell }} +
+ } @else { diff --git a/apps/demo-app/src/app/content/ui-components/table-content/tanstack-example/tanstack-example.component.ts b/apps/demo-app/src/app/content/ui-components/table-content/tanstack-example/tanstack-example.component.ts index 1293e2b6c..4f586542b 100644 --- a/apps/demo-app/src/app/content/ui-components/table-content/tanstack-example/tanstack-example.component.ts +++ b/apps/demo-app/src/app/content/ui-components/table-content/tanstack-example/tanstack-example.component.ts @@ -1,14 +1,13 @@ import { CommonModule } from '@angular/common'; import { ChangeDetectionStrategy, Component, signal } from '@angular/core'; -import { toObservable, toSignal } from '@angular/core/rxjs-interop'; import { ColumnDef, createAngularTable, FlexRenderDirective, getCoreRowModel, + getSortedRowModel, SortingState, } from '@tanstack/angular-table'; -import { combineLatest, map, of } from 'rxjs'; import { BarsSimpleStatesExampleComponent } from '../../../viz-components/bars-content/bars-simple-states-example/bars-simple-states-example.component'; type Person = { @@ -60,13 +59,15 @@ const defaultColumns: ColumnDef[] = [ accessorKey: 'firstName', cell: (info) => info.getValue(), footer: (info) => info.column.id, + enableSorting: false, }, { accessorFn: (row) => row.lastName, id: 'lastName', cell: (info) => `${info.getValue()}`, - header: () => `Last Name`, + header: () => `Last Name`, footer: (info) => info.column.id, + enableSorting: false, }, { accessorKey: 'age', @@ -77,13 +78,13 @@ const defaultColumns: ColumnDef[] = [ }, { accessorKey: 'performance.visits', - header: () => `Visits`, + header: () => `Visits`, footer: (info) => info.column.id, - sortingFn: (a, b) => { - const aValue = a.getValue('performance.visits'); - const bValue = b.getValue('performance.visits'); - return aValue - bValue; - }, + // sortingFn: (a, b) => { + // const aValue = a.getValue('performance.visits'); + // const bValue = b.getValue('performance.visits'); + // return aValue - bValue; + // }, }, { accessorKey: 'performance.status', @@ -93,7 +94,7 @@ const defaultColumns: ColumnDef[] = [ }, { accessorKey: 'performance.progress', - header: () => `Progress`, + header: () => `Progress`, footer: (info) => info.column.id, }, ]; @@ -122,21 +123,22 @@ export class TanstackExampleComponent { ]); //Use our controlled state values to fetch data - readonly data$ = combineLatest({ - data: of(defaultData), -= sorting: toObservable(this.sorting), - }).pipe(map(({ data, sorting }) => updateData(data, sorting))); + // readonly data$ = combineLatest({ + // data: of(defaultData), + // sorting: toObservable(this.sorting), + // }).pipe(map(({ data, sorting }) => updateData(data, sorting))); - readonly data = toSignal(this.data$); + readonly data = signal(defaultData); table = createAngularTable(() => ({ data: this.data(), columns: defaultColumns, getCoreRowModel: getCoreRowModel(), - initialState: { + getSortedRowModel: getSortedRowModel(), + state: { sorting: this.sorting(), }, + debugAll: true, enableSorting: true, - manualSorting: true, onSortingChange: (updater) => { if (updater instanceof Function) { this.sorting.update(updater); @@ -146,16 +148,16 @@ export class TanstackExampleComponent { }, })); } -function updateData(data: Person[], sorting: SortingState): Person[] { - if (sorting.length === 0) return data; +// function updateData(data: Person[], sorting: SortingState): Person[] { +// if (sorting.length === 0) return data; - const { id, desc } = sorting[0]; - return data.sort((a, b) => { - const aValue = id.split('.').reduce((obj, key) => obj[key], a); - const bValue = id.split('.').reduce((obj, key) => obj[key], b); +// const { id, desc } = sorting[0]; +// return data.sort((a, b) => { +// const aValue = id.split('.').reduce((obj, key) => obj[key], a); +// const bValue = id.split('.').reduce((obj, key) => obj[key], b); - if (aValue < bValue) return desc ? 1 : -1; - if (aValue > bValue) return desc ? -1 : 1; - return 0; - }); -} +// if (aValue < bValue) return desc ? 1 : -1; +// if (aValue > bValue) return desc ? -1 : 1; +// return 0; +// }); +// } From 3db2330f7133e25f6c35d0e16fd7e17dcf58e8bf Mon Sep 17 00:00:00 2001 From: Ally Choung Date: Mon, 9 Jun 2025 16:23:55 -0400 Subject: [PATCH 37/47] fix: clean up code, add styling icons --- .../tanstack-example.component.html | 15 +++- .../tanstack-example.component.scss | 82 ++++++++++++++++++- .../tanstack-example.component.ts | 37 ++------- 3 files changed, 102 insertions(+), 32 deletions(-) diff --git a/apps/demo-app/src/app/content/ui-components/table-content/tanstack-example/tanstack-example.component.html b/apps/demo-app/src/app/content/ui-components/table-content/tanstack-example/tanstack-example.component.html index 472a3429f..b21f786cb 100644 --- a/apps/demo-app/src/app/content/ui-components/table-content/tanstack-example/tanstack-example.component.html +++ b/apps/demo-app/src/app/content/ui-components/table-content/tanstack-example/tanstack-example.component.html @@ -15,7 +15,7 @@ " >
{{ headerCell }} + {{ sortIcon }}
diff --git a/apps/demo-app/src/app/content/ui-components/table-content/tanstack-example/tanstack-example.component.scss b/apps/demo-app/src/app/content/ui-components/table-content/tanstack-example/tanstack-example.component.scss index 76ff8ba14..5d074679d 100644 --- a/apps/demo-app/src/app/content/ui-components/table-content/tanstack-example/tanstack-example.component.scss +++ b/apps/demo-app/src/app/content/ui-components/table-content/tanstack-example/tanstack-example.component.scss @@ -1,3 +1,81 @@ -.sorted { - background-color: green; +@use 'vars' as *; +@use 'functions' as *; +@use 'colors'; +$icon-width: 0.9rem; +$icon-left-margin: 0.2rem; +$icon-right-margin: 0.4rem; + +.header-cell-sort { + display: flex; + &:hover { + cursor: pointer; + } + &.left { + align-items: flex-start; + justify-content: flex-start; + } + &.right { + align-items: flex-end; + justify-content: flex-end; + } +} + +.material-symbols-outlined { + display: flex; + justify-content: center; + width: $icon-width; + height: 1.2rem; + font-size: 1.25rem; + margin-left: $icon-left-margin; + margin-right: $icon-right-margin; + opacity: 0.25; + transition: all 150ms ease-in-out; + + &:hover { + opacity: 0.75; + } + + &.actively-sorted { + opacity: 1; + } +} + +.desc { + transform: rotate(180deg); +} + +.right { + text-align: right; +} + +.left { + text-align: left; +} + +.table-cell { + &.sorted-cell { + padding-right: $icon-left-margin + $icon-width + $icon-right-margin; + } +} + +.table-container { + border-spacing: 0; + td:last-child { + &.left { + padding-right: 0; + } + } + + th:last-child { + &.left { + padding-right: 0; + } + } + + th { + vertical-align: bottom; + &.sorted-header { + padding-right: 0; + } + } } diff --git a/apps/demo-app/src/app/content/ui-components/table-content/tanstack-example/tanstack-example.component.ts b/apps/demo-app/src/app/content/ui-components/table-content/tanstack-example/tanstack-example.component.ts index 4f586542b..5aa6ccb67 100644 --- a/apps/demo-app/src/app/content/ui-components/table-content/tanstack-example/tanstack-example.component.ts +++ b/apps/demo-app/src/app/content/ui-components/table-content/tanstack-example/tanstack-example.component.ts @@ -1,5 +1,10 @@ import { CommonModule } from '@angular/common'; -import { ChangeDetectionStrategy, Component, signal } from '@angular/core'; +import { + ChangeDetectionStrategy, + Component, + Input, + signal, +} from '@angular/core'; import { ColumnDef, createAngularTable, @@ -80,11 +85,6 @@ const defaultColumns: ColumnDef[] = [ accessorKey: 'performance.visits', header: () => `Visits`, footer: (info) => info.column.id, - // sortingFn: (a, b) => { - // const aValue = a.getValue('performance.visits'); - // const bValue = b.getValue('performance.visits'); - // return aValue - bValue; - // }, }, { accessorKey: 'performance.status', @@ -112,22 +112,14 @@ const defaultColumns: ColumnDef[] = [ styleUrls: ['./tanstack-example.component.scss'], }) export class TanstackExampleComponent { - onClick(id: string) { - console.log('onClick', id); - } + @Input() sortIcon: string = 'arrow_upward'; readonly sorting = signal([ { id: 'age', - desc: false, //sort by age in descending order by default + desc: false, }, ]); - //Use our controlled state values to fetch data - // readonly data$ = combineLatest({ - // data: of(defaultData), - // sorting: toObservable(this.sorting), - // }).pipe(map(({ data, sorting }) => updateData(data, sorting))); - readonly data = signal(defaultData); table = createAngularTable(() => ({ data: this.data(), @@ -148,16 +140,3 @@ export class TanstackExampleComponent { }, })); } -// function updateData(data: Person[], sorting: SortingState): Person[] { -// if (sorting.length === 0) return data; - -// const { id, desc } = sorting[0]; -// return data.sort((a, b) => { -// const aValue = id.split('.').reduce((obj, key) => obj[key], a); -// const bValue = id.split('.').reduce((obj, key) => obj[key], b); - -// if (aValue < bValue) return desc ? 1 : -1; -// if (aValue > bValue) return desc ? -1 : 1; -// return 0; -// }); -// } From c865463b53102c5bd471eda6b6659a2e32f12e78 Mon Sep 17 00:00:00 2001 From: tcoile Date: Tue, 15 Jul 2025 13:17:20 -0700 Subject: [PATCH 38/47] fix: comment out broken code whoop --- libs/ui-components/src/lib/table/table-column.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/ui-components/src/lib/table/table-column.ts b/libs/ui-components/src/lib/table/table-column.ts index 7dab9edc3..06724e5a4 100644 --- a/libs/ui-components/src/lib/table/table-column.ts +++ b/libs/ui-components/src/lib/table/table-column.ts @@ -60,7 +60,7 @@ export class TableColumn { sortedOnInit = false; constructor(init?: Partial>) { this.sortDirection = SortDirection.asc; - this.getAlignment = () => 'left'; + // this.getAlignment = () => 'left'; safeAssign(this, init); this.initialSortDirection = this.sortDirection; if (this.ascendingSortFunction === undefined) { From 1fcb94557e49296b7105db3468147b46a60180cc Mon Sep 17 00:00:00 2001 From: tcoile Date: Tue, 15 Jul 2025 22:39:56 -0700 Subject: [PATCH 39/47] docs: add charts to tanstack example --- .../tanstack-example.component.html | 22 ++- .../tanstack-example.component.ts | 129 ++++++++++++------ 2 files changed, 100 insertions(+), 51 deletions(-) diff --git a/apps/demo-app/src/app/content/ui-components/table-content/tanstack-example/tanstack-example.component.html b/apps/demo-app/src/app/content/ui-components/table-content/tanstack-example/tanstack-example.component.html index b21f786cb..ca6a919eb 100644 --- a/apps/demo-app/src/app/content/ui-components/table-content/tanstack-example/tanstack-example.component.html +++ b/apps/demo-app/src/app/content/ui-components/table-content/tanstack-example/tanstack-example.component.html @@ -63,14 +63,22 @@ - + + + + + + + + {{ cell.getValue() }} diff --git a/apps/demo-app/src/app/content/ui-components/table-content/tanstack-example/tanstack-example.component.ts b/apps/demo-app/src/app/content/ui-components/table-content/tanstack-example/tanstack-example.component.ts index 5aa6ccb67..9ee27e729 100644 --- a/apps/demo-app/src/app/content/ui-components/table-content/tanstack-example/tanstack-example.component.ts +++ b/apps/demo-app/src/app/content/ui-components/table-content/tanstack-example/tanstack-example.component.ts @@ -3,8 +3,22 @@ import { ChangeDetectionStrategy, Component, Input, + OnInit, signal, } from '@angular/core'; +import { + ChartConfig, + LinesConfig, + VicChartConfigBuilder, + VicChartModule, + VicLinesConfigBuilder, + VicLinesModule, + VicXQuantitativeAxisConfig, + VicXQuantitativeAxisConfigBuilder, + VicXyAxisModule, + VicYQuantitativeAxisConfig, + VicYQuantitativeAxisConfigBuilder, +} from '@hsi/viz-components'; import { ColumnDef, createAngularTable, @@ -13,49 +27,53 @@ import { getSortedRowModel, SortingState, } from '@tanstack/angular-table'; -import { BarsSimpleStatesExampleComponent } from '../../../viz-components/bars-content/bars-simple-states-example/bars-simple-states-example.component'; +type PerformanceReview = { + year: number; + hrAppraisal: number; + employeeAppraisal: number; +}; type Person = { firstName: string; lastName: string; age: number; - performance: { - visits: number; - status: string; - progress: number; - }; + performance: PerformanceReview[]; +}; + +type PersonWithCharts = Person & { + chartConfig: LinesConfig; }; const defaultData: Person[] = [ { - firstName: 'tanner', - lastName: 'linsley', - age: 24, - performance: { - visits: 100, - status: 'In Relationship', - progress: 50, - }, + firstName: 'first', + lastName: 'last', + age: 106, + performance: [ + { year: 2020, hrAppraisal: 9, employeeAppraisal: 4 }, + { year: 2023, hrAppraisal: 10, employeeAppraisal: 2 }, + { year: 2025, hrAppraisal: 10, employeeAppraisal: -10 }, + ], }, { - firstName: 'joe', - lastName: 'dirte', + firstName: 'person', + lastName: 'name', age: 45, - performance: { - visits: 20, - status: 'Single', - progress: 10, - }, + performance: [ + { year: 2020, hrAppraisal: 8, employeeAppraisal: 9 }, + { year: 2023, hrAppraisal: 10, employeeAppraisal: 10 }, + { year: 2025, hrAppraisal: 0, employeeAppraisal: 10 }, + ], }, { - firstName: 'tandy', - lastName: 'miller', + firstName: 'another', + lastName: 'name', age: 40, - performance: { - visits: 40, - status: 'Single', - progress: 80, - }, + performance: [ + { year: 2020, hrAppraisal: 8, employeeAppraisal: 5 }, + { year: 2023, hrAppraisal: 6, employeeAppraisal: 3 }, + { year: 2025, hrAppraisal: 7, employeeAppraisal: 1 }, + ], }, ]; @@ -82,19 +100,8 @@ const defaultColumns: ColumnDef[] = [ enableSorting: true, }, { - accessorKey: 'performance.visits', - header: () => `Visits`, - footer: (info) => info.column.id, - }, - { - accessorKey: 'performance.status', - header: 'Status', - footer: (info) => info.column.id, - sortingFn: 'basic', - }, - { - accessorKey: 'performance.progress', - header: () => `Progress`, + accessorKey: 'chartConfig', + header: () => `performance appraisal`, footer: (info) => info.column.id, }, ]; @@ -105,13 +112,21 @@ const defaultColumns: ColumnDef[] = [ imports: [ FlexRenderDirective, CommonModule, - BarsSimpleStatesExampleComponent, + VicChartModule, + VicLinesModule, + VicXyAxisModule, + ], + providers: [ + VicChartConfigBuilder, + VicLinesConfigBuilder, + VicYQuantitativeAxisConfigBuilder, + VicXQuantitativeAxisConfigBuilder, ], templateUrl: './tanstack-example.component.html', changeDetection: ChangeDetectionStrategy.OnPush, styleUrls: ['./tanstack-example.component.scss'], }) -export class TanstackExampleComponent { +export class TanstackExampleComponent implements OnInit { @Input() sortIcon: string = 'arrow_upward'; readonly sorting = signal([ { @@ -120,7 +135,8 @@ export class TanstackExampleComponent { }, ]); - readonly data = signal(defaultData); + data = signal(null); + table = createAngularTable(() => ({ data: this.data(), columns: defaultColumns, @@ -139,4 +155,29 @@ export class TanstackExampleComponent { } }, })); + + chartConfig: ChartConfig; + xAxisQuantitativeConfig: VicXQuantitativeAxisConfig; + yAxisConfig: VicYQuantitativeAxisConfig; + + ngOnInit(): void { + this.data.set(this.addChartsToData(defaultData)); + this.chartConfig = new VicChartConfigBuilder().getConfig(); + this.xAxisQuantitativeConfig = new VicXQuantitativeAxisConfigBuilder() + .ticks((ticks) => ticks.format('%Y')) + .getConfig(); + this.yAxisConfig = new VicYQuantitativeAxisConfigBuilder().getConfig(); + } + + addChartsToData(data: Person[]): PersonWithCharts[] { + return data.map((person) => { + const chartConfig = new VicLinesConfigBuilder() + .data(person.performance) + .xDate((xDate) => xDate.valueAccessor((d) => new Date(d.year))) + .y((yValue) => yValue.valueAccessor((d) => d.employeeAppraisal)) + .pointMarkers((markers) => markers.radius(2).growByOnHover(3)) + .getConfig(); + return { ...person, chartConfig }; + }); + } } From f89a168937ed75eed288bad3f14f3b76f66e2ed6 Mon Sep 17 00:00:00 2001 From: Ally Choung Date: Wed, 16 Jul 2025 12:22:50 -0400 Subject: [PATCH 40/47] fix: remove old key value pipe --- .../table-example/table-example.component.ts | 3 +-- .../lib/core/pipes/get-value-by-key.pipe.ts | 24 ------------------- libs/app-dev-kit/src/lib/core/pipes/index.ts | 1 - libs/app-dev-kit/src/public-api.ts | 1 - 4 files changed, 1 insertion(+), 28 deletions(-) delete mode 100644 libs/app-dev-kit/src/lib/core/pipes/get-value-by-key.pipe.ts delete mode 100644 libs/app-dev-kit/src/lib/core/pipes/index.ts diff --git a/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.ts b/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.ts index 623db2072..d8577f02b 100644 --- a/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.ts +++ b/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.ts @@ -5,7 +5,6 @@ import { Input, OnInit, } from '@angular/core'; -import { GetValueByKeyPipe } from '@hsi/app-dev-kit'; import { HsiUiTableDataSource, TableColumnsBuilder, @@ -39,7 +38,7 @@ class FruitType { @Component({ selector: 'app-table-example', standalone: true, - imports: [CommonModule, TableModule, GetValueByKeyPipe], + imports: [CommonModule, TableModule], templateUrl: './table-example.component.html', styleUrls: ['../../../examples.scss', './table-example.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, diff --git a/libs/app-dev-kit/src/lib/core/pipes/get-value-by-key.pipe.ts b/libs/app-dev-kit/src/lib/core/pipes/get-value-by-key.pipe.ts deleted file mode 100644 index ad9cbcc7e..000000000 --- a/libs/app-dev-kit/src/lib/core/pipes/get-value-by-key.pipe.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { Pipe, PipeTransform } from '@angular/core'; -/** - * Retrieves the key in the object. - */ -@Pipe({ - name: 'getValueByKey', - standalone: true, -}) -export class GetValueByKeyPipe implements PipeTransform { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - transform(element: any, key: string): any { - if (!element || !key) { - throw new Error('Invalid input: element and key are required'); - } - key.split('.').forEach((k) => { - if (element && typeof element === 'object') { - element = element[k]; - } else { - throw new Error(`Subkey "${k}" not found in the object`); - } - }); - return element; - } -} diff --git a/libs/app-dev-kit/src/lib/core/pipes/index.ts b/libs/app-dev-kit/src/lib/core/pipes/index.ts deleted file mode 100644 index af68a92b4..000000000 --- a/libs/app-dev-kit/src/lib/core/pipes/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './get-value-by-key.pipe'; diff --git a/libs/app-dev-kit/src/public-api.ts b/libs/app-dev-kit/src/public-api.ts index b40d20a31..ac728af75 100644 --- a/libs/app-dev-kit/src/public-api.ts +++ b/libs/app-dev-kit/src/public-api.ts @@ -1,6 +1,5 @@ export * from './lib/assets'; export * from './lib/content-parsing'; -export * from './lib/core/pipes/index'; export * from './lib/core/utilities/index'; export * from './lib/documentation-display'; export * from './lib/ng-utilities'; From c067248144003c89355a4f975288d0224603367f Mon Sep 17 00:00:00 2001 From: Ally Choung Date: Wed, 16 Jul 2025 12:23:59 -0400 Subject: [PATCH 41/47] fix: remove old table example --- .../table-example.component.html | 50 -------- .../table-example.component.scss | 81 ------------ .../table-example/table-example.component.ts | 120 ------------------ 3 files changed, 251 deletions(-) delete mode 100644 apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.html delete mode 100644 apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.scss delete mode 100644 apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.ts diff --git a/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.html b/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.html deleted file mode 100644 index 790fbe195..000000000 --- a/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.html +++ /dev/null @@ -1,50 +0,0 @@ -@if (dataSource.columns$ | async; as columns) { - - @for (column of columns; track column.id) { - - @if (column.sortable) { - - } @else { - - } - - - } - - -
-
- {{ column.label }} - {{ sortIcon }} -
- {{ column.label }} - - {{ element | getValueByKey: column.key }} -
-} diff --git a/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.scss b/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.scss deleted file mode 100644 index 5d074679d..000000000 --- a/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.scss +++ /dev/null @@ -1,81 +0,0 @@ -@use 'vars' as *; -@use 'functions' as *; -@use 'colors'; -$icon-width: 0.9rem; -$icon-left-margin: 0.2rem; -$icon-right-margin: 0.4rem; - -.header-cell-sort { - display: flex; - &:hover { - cursor: pointer; - } - &.left { - align-items: flex-start; - justify-content: flex-start; - } - &.right { - align-items: flex-end; - justify-content: flex-end; - } -} - -.material-symbols-outlined { - display: flex; - justify-content: center; - width: $icon-width; - height: 1.2rem; - font-size: 1.25rem; - margin-left: $icon-left-margin; - margin-right: $icon-right-margin; - opacity: 0.25; - transition: all 150ms ease-in-out; - - &:hover { - opacity: 0.75; - } - - &.actively-sorted { - opacity: 1; - } -} - -.desc { - transform: rotate(180deg); -} - -.right { - text-align: right; -} - -.left { - text-align: left; -} - -.table-cell { - &.sorted-cell { - padding-right: $icon-left-margin + $icon-width + $icon-right-margin; - } -} - -.table-container { - border-spacing: 0; - td:last-child { - &.left { - padding-right: 0; - } - } - - th:last-child { - &.left { - padding-right: 0; - } - } - - th { - vertical-align: bottom; - &.sorted-header { - padding-right: 0; - } - } -} diff --git a/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.ts b/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.ts deleted file mode 100644 index d8577f02b..000000000 --- a/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { CommonModule } from '@angular/common'; -import { - ChangeDetectionStrategy, - Component, - Input, - OnInit, -} from '@angular/core'; -import { - HsiUiTableDataSource, - TableColumnsBuilder, - TableModule, -} from '@hsi/ui-components'; -import { of } from 'rxjs'; - -enum ColumnNames { - fruit = 'Fruit', - colorName = 'Color', - inventory = 'Inventory', - price = 'Sell price', -} - -enum FruitInfo { - fruit = 'fruit', - colorName = 'color', - inventory = 'metrics.inventory', - price = 'metrics.price', -} - -class FruitType { - fruit: string; - color: string; - metrics: { - inventory: number; - price: number; - }; -} - -@Component({ - selector: 'app-table-example', - standalone: true, - imports: [CommonModule, TableModule], - templateUrl: './table-example.component.html', - styleUrls: ['../../../examples.scss', './table-example.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class TableExampleComponent implements OnInit { - @Input() sortIcon: string = 'arrow_upward'; - dataSource: HsiUiTableDataSource; - - ngOnInit(): void { - this.setTableData(); - } - - setTableData() { - const initData$ = of([ - { - fruit: 'lemon', - color: 'yellow', - metrics: { inventory: 10, price: 1.2 }, - }, - { - fruit: 'mango', - color: 'orange', - metrics: { inventory: 5, price: 2.5 }, - }, - { - fruit: 'avocado', - color: 'green', - metrics: { inventory: 20, price: 3.0 }, - }, - { - fruit: 'apple', - color: 'red', - metrics: { inventory: 15, price: 1.5 }, - }, - { - fruit: 'orange', - color: 'orange', - metrics: { inventory: 20, price: 1.8 }, - }, - { - fruit: 'banana', - color: 'yellow', - metrics: { inventory: 5, price: 1.0 }, - }, - ]); - const initColumns$ = of( - new TableColumnsBuilder() - .addColumn( - (column) => - column - .label(ColumnNames.fruit) - .cssClass('left') - .displayKey(FruitInfo.fruit) - .ascendingSortFunction((a, b) => a.fruit.localeCompare(b.fruit)) - .sortOrder(1) - .sortable(true) - .sortDirection('asc') // initial sort direction - ) - .addColumn((column) => - column - .displayKey(FruitInfo.colorName) - .label(ColumnNames.colorName) - .cssClass('left') - .sortable(true) - .ascendingSortFunction((a, b) => a.color.localeCompare(b.color)) - ) - .addColumn((column) => - column - .displayKey(FruitInfo.inventory) - .label(ColumnNames.inventory) - .cssClass('right') - .sortable(true) - .getSortValue((x) => x.metrics.inventory) - ) - .getConfig() - ); - this.dataSource = new HsiUiTableDataSource(initData$, initColumns$); - } -} From 191a798784bce27f05964c62e74a899d828e6e68 Mon Sep 17 00:00:00 2001 From: Ally Choung Date: Fri, 18 Jul 2025 11:32:09 -0400 Subject: [PATCH 42/47] Revert "fix: remove old table example" This reverts commit c067248144003c89355a4f975288d0224603367f. --- .../table-example.component.html | 50 ++++++++ .../table-example.component.scss | 81 ++++++++++++ .../table-example/table-example.component.ts | 120 ++++++++++++++++++ 3 files changed, 251 insertions(+) create mode 100644 apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.html create mode 100644 apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.scss create mode 100644 apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.ts diff --git a/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.html b/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.html new file mode 100644 index 000000000..790fbe195 --- /dev/null +++ b/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.html @@ -0,0 +1,50 @@ +@if (dataSource.columns$ | async; as columns) { + + @for (column of columns; track column.id) { + + @if (column.sortable) { + + } @else { + + } + + + } + + +
+
+ {{ column.label }} + {{ sortIcon }} +
+ {{ column.label }} + + {{ element | getValueByKey: column.key }} +
+} diff --git a/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.scss b/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.scss new file mode 100644 index 000000000..5d074679d --- /dev/null +++ b/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.scss @@ -0,0 +1,81 @@ +@use 'vars' as *; +@use 'functions' as *; +@use 'colors'; +$icon-width: 0.9rem; +$icon-left-margin: 0.2rem; +$icon-right-margin: 0.4rem; + +.header-cell-sort { + display: flex; + &:hover { + cursor: pointer; + } + &.left { + align-items: flex-start; + justify-content: flex-start; + } + &.right { + align-items: flex-end; + justify-content: flex-end; + } +} + +.material-symbols-outlined { + display: flex; + justify-content: center; + width: $icon-width; + height: 1.2rem; + font-size: 1.25rem; + margin-left: $icon-left-margin; + margin-right: $icon-right-margin; + opacity: 0.25; + transition: all 150ms ease-in-out; + + &:hover { + opacity: 0.75; + } + + &.actively-sorted { + opacity: 1; + } +} + +.desc { + transform: rotate(180deg); +} + +.right { + text-align: right; +} + +.left { + text-align: left; +} + +.table-cell { + &.sorted-cell { + padding-right: $icon-left-margin + $icon-width + $icon-right-margin; + } +} + +.table-container { + border-spacing: 0; + td:last-child { + &.left { + padding-right: 0; + } + } + + th:last-child { + &.left { + padding-right: 0; + } + } + + th { + vertical-align: bottom; + &.sorted-header { + padding-right: 0; + } + } +} diff --git a/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.ts b/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.ts new file mode 100644 index 000000000..d8577f02b --- /dev/null +++ b/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.ts @@ -0,0 +1,120 @@ +import { CommonModule } from '@angular/common'; +import { + ChangeDetectionStrategy, + Component, + Input, + OnInit, +} from '@angular/core'; +import { + HsiUiTableDataSource, + TableColumnsBuilder, + TableModule, +} from '@hsi/ui-components'; +import { of } from 'rxjs'; + +enum ColumnNames { + fruit = 'Fruit', + colorName = 'Color', + inventory = 'Inventory', + price = 'Sell price', +} + +enum FruitInfo { + fruit = 'fruit', + colorName = 'color', + inventory = 'metrics.inventory', + price = 'metrics.price', +} + +class FruitType { + fruit: string; + color: string; + metrics: { + inventory: number; + price: number; + }; +} + +@Component({ + selector: 'app-table-example', + standalone: true, + imports: [CommonModule, TableModule], + templateUrl: './table-example.component.html', + styleUrls: ['../../../examples.scss', './table-example.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class TableExampleComponent implements OnInit { + @Input() sortIcon: string = 'arrow_upward'; + dataSource: HsiUiTableDataSource; + + ngOnInit(): void { + this.setTableData(); + } + + setTableData() { + const initData$ = of([ + { + fruit: 'lemon', + color: 'yellow', + metrics: { inventory: 10, price: 1.2 }, + }, + { + fruit: 'mango', + color: 'orange', + metrics: { inventory: 5, price: 2.5 }, + }, + { + fruit: 'avocado', + color: 'green', + metrics: { inventory: 20, price: 3.0 }, + }, + { + fruit: 'apple', + color: 'red', + metrics: { inventory: 15, price: 1.5 }, + }, + { + fruit: 'orange', + color: 'orange', + metrics: { inventory: 20, price: 1.8 }, + }, + { + fruit: 'banana', + color: 'yellow', + metrics: { inventory: 5, price: 1.0 }, + }, + ]); + const initColumns$ = of( + new TableColumnsBuilder() + .addColumn( + (column) => + column + .label(ColumnNames.fruit) + .cssClass('left') + .displayKey(FruitInfo.fruit) + .ascendingSortFunction((a, b) => a.fruit.localeCompare(b.fruit)) + .sortOrder(1) + .sortable(true) + .sortDirection('asc') // initial sort direction + ) + .addColumn((column) => + column + .displayKey(FruitInfo.colorName) + .label(ColumnNames.colorName) + .cssClass('left') + .sortable(true) + .ascendingSortFunction((a, b) => a.color.localeCompare(b.color)) + ) + .addColumn((column) => + column + .displayKey(FruitInfo.inventory) + .label(ColumnNames.inventory) + .cssClass('right') + .sortable(true) + .getSortValue((x) => x.metrics.inventory) + ) + .getConfig() + ); + this.dataSource = new HsiUiTableDataSource(initData$, initColumns$); + } +} From 43c656348983350646ba26e6eb67660d89967ceb Mon Sep 17 00:00:00 2001 From: Ally Choung Date: Fri, 18 Jul 2025 11:32:25 -0400 Subject: [PATCH 43/47] Revert "fix: remove old key value pipe" This reverts commit f89a168937ed75eed288bad3f14f3b76f66e2ed6. --- .../table-example/table-example.component.ts | 3 ++- .../lib/core/pipes/get-value-by-key.pipe.ts | 24 +++++++++++++++++++ libs/app-dev-kit/src/lib/core/pipes/index.ts | 1 + libs/app-dev-kit/src/public-api.ts | 1 + 4 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 libs/app-dev-kit/src/lib/core/pipes/get-value-by-key.pipe.ts create mode 100644 libs/app-dev-kit/src/lib/core/pipes/index.ts diff --git a/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.ts b/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.ts index d8577f02b..623db2072 100644 --- a/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.ts +++ b/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.ts @@ -5,6 +5,7 @@ import { Input, OnInit, } from '@angular/core'; +import { GetValueByKeyPipe } from '@hsi/app-dev-kit'; import { HsiUiTableDataSource, TableColumnsBuilder, @@ -38,7 +39,7 @@ class FruitType { @Component({ selector: 'app-table-example', standalone: true, - imports: [CommonModule, TableModule], + imports: [CommonModule, TableModule, GetValueByKeyPipe], templateUrl: './table-example.component.html', styleUrls: ['../../../examples.scss', './table-example.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, diff --git a/libs/app-dev-kit/src/lib/core/pipes/get-value-by-key.pipe.ts b/libs/app-dev-kit/src/lib/core/pipes/get-value-by-key.pipe.ts new file mode 100644 index 000000000..ad9cbcc7e --- /dev/null +++ b/libs/app-dev-kit/src/lib/core/pipes/get-value-by-key.pipe.ts @@ -0,0 +1,24 @@ +import { Pipe, PipeTransform } from '@angular/core'; +/** + * Retrieves the key in the object. + */ +@Pipe({ + name: 'getValueByKey', + standalone: true, +}) +export class GetValueByKeyPipe implements PipeTransform { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + transform(element: any, key: string): any { + if (!element || !key) { + throw new Error('Invalid input: element and key are required'); + } + key.split('.').forEach((k) => { + if (element && typeof element === 'object') { + element = element[k]; + } else { + throw new Error(`Subkey "${k}" not found in the object`); + } + }); + return element; + } +} diff --git a/libs/app-dev-kit/src/lib/core/pipes/index.ts b/libs/app-dev-kit/src/lib/core/pipes/index.ts new file mode 100644 index 000000000..af68a92b4 --- /dev/null +++ b/libs/app-dev-kit/src/lib/core/pipes/index.ts @@ -0,0 +1 @@ +export * from './get-value-by-key.pipe'; diff --git a/libs/app-dev-kit/src/public-api.ts b/libs/app-dev-kit/src/public-api.ts index ac728af75..b40d20a31 100644 --- a/libs/app-dev-kit/src/public-api.ts +++ b/libs/app-dev-kit/src/public-api.ts @@ -1,5 +1,6 @@ export * from './lib/assets'; export * from './lib/content-parsing'; +export * from './lib/core/pipes/index'; export * from './lib/core/utilities/index'; export * from './lib/documentation-display'; export * from './lib/ng-utilities'; From 9fd6382432a0b042e0906b40fb490f10ef0931c6 Mon Sep 17 00:00:00 2001 From: Ally Choung Date: Fri, 18 Jul 2025 11:45:07 -0400 Subject: [PATCH 44/47] fix: add back table example component This reverts commit d01e7d37ca44056b52dcfe9ec2afb0081c2c6d46. --- .../table-example.component.html | 9 ++-- .../table-example.component.scss | 18 ++------ .../table-example/table-example.component.ts | 3 -- .../src/assets/ui-components/content/table.md | 45 +++---------------- .../src/lib/table/table-column-builder.ts | 13 ------ .../src/lib/table/table-column.ts | 6 +-- 6 files changed, 13 insertions(+), 81 deletions(-) diff --git a/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.html b/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.html index 790fbe195..ddd30645d 100644 --- a/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.html +++ b/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.html @@ -9,7 +9,7 @@ *cdkHeaderCellDef="let element" (click)="dataSource.sort(column)" > -
+
{{ column.label }} diff --git a/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.scss b/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.scss index 5d074679d..381f55b6b 100644 --- a/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.scss +++ b/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.scss @@ -7,17 +7,11 @@ $icon-right-margin: 0.4rem; .header-cell-sort { display: flex; + align-items: flex-end; + justify-content: flex-end; &:hover { cursor: pointer; } - &.left { - align-items: flex-start; - justify-content: flex-start; - } - &.right { - align-items: flex-end; - justify-content: flex-end; - } } .material-symbols-outlined { @@ -44,15 +38,9 @@ $icon-right-margin: 0.4rem; transform: rotate(180deg); } -.right { +.table-cell { text-align: right; -} -.left { - text-align: left; -} - -.table-cell { &.sorted-cell { padding-right: $icon-left-margin + $icon-width + $icon-right-margin; } diff --git a/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.ts b/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.ts index 623db2072..f3be51978 100644 --- a/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.ts +++ b/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.ts @@ -91,7 +91,6 @@ export class TableExampleComponent implements OnInit { (column) => column .label(ColumnNames.fruit) - .cssClass('left') .displayKey(FruitInfo.fruit) .ascendingSortFunction((a, b) => a.fruit.localeCompare(b.fruit)) .sortOrder(1) @@ -102,7 +101,6 @@ export class TableExampleComponent implements OnInit { column .displayKey(FruitInfo.colorName) .label(ColumnNames.colorName) - .cssClass('left') .sortable(true) .ascendingSortFunction((a, b) => a.color.localeCompare(b.color)) ) @@ -110,7 +108,6 @@ export class TableExampleComponent implements OnInit { column .displayKey(FruitInfo.inventory) .label(ColumnNames.inventory) - .cssClass('right') .sortable(true) .getSortValue((x) => x.metrics.inventory) ) diff --git a/apps/demo-app/src/assets/ui-components/content/table.md b/apps/demo-app/src/assets/ui-components/content/table.md index ad62c5f8b..2db81316c 100644 --- a/apps/demo-app/src/assets/ui-components/content/table.md +++ b/apps/demo-app/src/assets/ui-components/content/table.md @@ -114,7 +114,6 @@ export class TableExampleComponent implements OnInit { (column) => column .label(ColumnNames.fruit) - .cssClass('left') .displayKey(FruitInfo.fruit) .ascendingSortFunction((a, b) => a.fruit.localeCompare(b.fruit)) .sortOrder(1) @@ -125,7 +124,6 @@ export class TableExampleComponent implements OnInit { column .displayKey(FruitInfo.colorName) .label(ColumnNames.colorName) - .cssClass('left') .sortable(true) .ascendingSortFunction((a, b) => a.color.localeCompare(b.color)) ) @@ -133,7 +131,6 @@ export class TableExampleComponent implements OnInit { column .displayKey(FruitInfo.inventory) .label(ColumnNames.inventory) - .cssClass('right') .sortable(true) .getSortValue((x) => x.metrics.inventory) ) @@ -156,7 +153,7 @@ export class TableExampleComponent implements OnInit { *cdkHeaderCellDef="let element" (click)="dataSource.sort(column)" > -
+
{{ column.label }} {{ column.label }} } - + {{ element | getValueByKey: column.key }} @@ -344,17 +333,7 @@ params: - name: label type: string description: - - 'The label to be used by the table column.' -``` - -```builder-method -name: cssClass -description: 'Sets the CSS class of the table column.' -params: - - name: cssClass - type: string - description: - - 'The CSS class to be used by the table column.' + - 'The label to be used by the table column' ``` ```builder-method @@ -471,17 +450,11 @@ $icon-right-margin: 0.4rem; .header-cell-sort { display: flex; + align-items: flex-end; + justify-content: flex-end; &:hover { cursor: pointer; } - &.left { - align-items: flex-start; - justify-content: flex-start; - } - &.right { - align-items: flex-end; - justify-content: flex-end; - } } .material-symbols-outlined { @@ -508,15 +481,9 @@ $icon-right-margin: 0.4rem; transform: rotate(180deg); } -.right { +.table-cell { text-align: right; -} -.left { - text-align: left; -} - -.table-cell { &.sorted-cell { padding-right: $icon-left-margin + $icon-width + $icon-right-margin; } diff --git a/libs/ui-components/src/lib/table/table-column-builder.ts b/libs/ui-components/src/lib/table/table-column-builder.ts index 28cd2a97a..e378520fa 100644 --- a/libs/ui-components/src/lib/table/table-column-builder.ts +++ b/libs/ui-components/src/lib/table/table-column-builder.ts @@ -6,7 +6,6 @@ import { } from './table-column'; const DEFAULT = { - _cssClass: '', _sortable: false, _sortedOnInit: false, _sortOrder: Number.MAX_SAFE_INTEGER, @@ -17,7 +16,6 @@ const DEFAULT = { * An internal builder class for a single table column. */ export class TableColumnBuilder { - private _cssClass: string; private _id: string; private _key: string; private _label: string; @@ -31,16 +29,6 @@ export class TableColumnBuilder { Object.assign(this, DEFAULT); } - /** - * OPTIONAL. Determines additional CSS classes to be applied to the table column. - * - * @param cssClass A string to use as the CSS class of the table column. - */ - cssClass(cssClass: string): this { - this._cssClass = cssClass; - return this; - } - /** * REQUIRED. Determines the id of the table column. * @@ -157,7 +145,6 @@ export class TableColumnBuilder { id: this._id, label: this._label, key: this._key, - cssClass: this._cssClass, getSortValue: this._getSortValue, ascendingSortFunction: this._ascendingSortFunction, sortDirection: this._sortDirection, diff --git a/libs/ui-components/src/lib/table/table-column.ts b/libs/ui-components/src/lib/table/table-column.ts index 06724e5a4..1011e0051 100644 --- a/libs/ui-components/src/lib/table/table-column.ts +++ b/libs/ui-components/src/lib/table/table-column.ts @@ -10,17 +10,13 @@ export type SortDirectionType = keyof typeof SortDirection; export type TableValue = string | number | boolean | Date; export class TableColumn { - /** - * The CSS class of the column. - * @default '' - */ - cssClass: string = ''; /** * The unique id of the column. * */ readonly id: string; /** * The unique key of the column. Used for sorting and accessing the column data. + * */ readonly key: string; /** From 53e93ff7df1b457976bb622ea474ff650ca27847 Mon Sep 17 00:00:00 2001 From: Ally Choung Date: Mon, 21 Jul 2025 16:14:26 -0400 Subject: [PATCH 45/47] docs: mark old table as deprecated --- .../table-content.component.html | 12 +- .../table-content/table-content.component.ts | 2 + .../table-example/table-example.component.ts | 3 + apps/demo-app/src/assets/content.yaml | 4 +- .../ui-components/content/deprecated-table.md | 507 +++++++++++++++++ .../src/assets/ui-components/content/table.md | 513 ------------------ .../lib/core/pipes/get-value-by-key.pipe.ts | 1 + .../src/lib/table/table-column.ts | 3 + .../src/lib/table/table.data-source.ts | 3 + 9 files changed, 533 insertions(+), 515 deletions(-) create mode 100644 apps/demo-app/src/assets/ui-components/content/deprecated-table.md diff --git a/apps/demo-app/src/app/content/ui-components/table-content/table-content.component.html b/apps/demo-app/src/app/content/ui-components/table-content/table-content.component.html index 8834c0241..1909f6157 100644 --- a/apps/demo-app/src/app/content/ui-components/table-content/table-content.component.html +++ b/apps/demo-app/src/app/content/ui-components/table-content/table-content.component.html @@ -6,11 +6,21 @@ height="600px" path="app/content/ui-components/table-content/tanstack-example" class="example" - label="table example" + label="Tanstack table example" > } + @case ('deprecated simple table') { + + + + } } diff --git a/apps/demo-app/src/app/content/ui-components/table-content/table-content.component.ts b/apps/demo-app/src/app/content/ui-components/table-content/table-content.component.ts index ddf53d89b..a3fed6158 100644 --- a/apps/demo-app/src/app/content/ui-components/table-content/table-content.component.ts +++ b/apps/demo-app/src/app/content/ui-components/table-content/table-content.component.ts @@ -6,6 +6,7 @@ import { } from '@angular/core'; import { SinglePanelExampleDisplayComponent } from '../../../platform/single-panel-example-display/single-panel-example-display.component'; import { ContentContainerComponent } from '../../content-container/content-container.component'; +import { TableExampleComponent } from './table-example/table-example.component'; import { TanstackExampleComponent } from './tanstack-example/tanstack-example.component'; @Component({ @@ -15,6 +16,7 @@ import { TanstackExampleComponent } from './tanstack-example/tanstack-example.co SinglePanelExampleDisplayComponent, TanstackExampleComponent, ContentContainerComponent, + TableExampleComponent, ], templateUrl: './table-content.component.html', styleUrls: [ diff --git a/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.ts b/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.ts index f3be51978..b04acc942 100644 --- a/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.ts +++ b/apps/demo-app/src/app/content/ui-components/table-content/table-example/table-example.component.ts @@ -36,6 +36,9 @@ class FruitType { }; } +/** + * @deprecated This component is deprecated. See `tanstack-example.component.ts` for the recommended implementation. + */ @Component({ selector: 'app-table-example', standalone: true, diff --git a/apps/demo-app/src/assets/content.yaml b/apps/demo-app/src/assets/content.yaml index d4bca5b29..8b3037754 100644 --- a/apps/demo-app/src/assets/content.yaml +++ b/apps/demo-app/src/assets/content.yaml @@ -24,7 +24,9 @@ ui-components: title: UI Components items: overview: overview.md - table: table.md + table: + tanstack-table: table.md + deprecated-simple-table: deprecated-table.md combobox: overview: combobox.md filtering-options: filtering-options.md diff --git a/apps/demo-app/src/assets/ui-components/content/deprecated-table.md b/apps/demo-app/src/assets/ui-components/content/deprecated-table.md new file mode 100644 index 000000000..ac7c92ca1 --- /dev/null +++ b/apps/demo-app/src/assets/ui-components/content/deprecated-table.md @@ -0,0 +1,507 @@ +# Table Component + +HSI UI Components provides a set of Angular components and configuration classes that can be used to +create tables. These components can be imported via the `TableModule`. + +The HSI UI Components table uses the +[Angular Material CDK Table](https://material.angular.io/cdk/table/overview) as a reference. + +## Composing a Table + +A table is minimally composed of the following HTML components: + +- `table` — A component that is an outer wrapper for other components in the table. +- `th` — A component that represents a header cell in the table. +- `td` — A component that represents a data cell in the table. +- `tr` — A component that represents a row of cells in the table. + +In addition, the table must also be given data through an `HsiUiTableDataSource` instance. + +The following is a minimal implementation: + +```custom-angular +deprecated simple table +``` + +```ts +import { CommonModule } from '@angular/common'; +import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core'; +import { GetValueByKeyPipe } from '@hsi/app-dev-kit'; +import { HsiUiTableDataSource, TableColumnsBuilder, TableModule } from '@hsi/ui-components'; +import { of } from 'rxjs'; + +enum ColumnNames { + fruit = 'Fruit', + colorName = 'Color', + inventory = 'Inventory', + price = 'Sell price', +} + +enum FruitInfo { + fruit = 'fruit', + colorName = 'color', + inventory = 'metrics.inventory', + price = 'metrics.price', +} + +class FruitType { + fruit: string; + color: string; + metrics: { + inventory: number; + price: number; + }; +} + +@Component({ + selector: 'app-table-example', + standalone: true, + imports: [CommonModule, TableModule, GetValueByKeyPipe], + templateUrl: './table-example.component.html', + styleUrls: ['../../../examples.scss', './table-example.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class TableExampleComponent implements OnInit { + @Input() sortIcon: string = 'arrow_upward'; + dataSource: HsiUiTableDataSource; + + ngOnInit(): void { + this.setTableData(); + } + + setTableData() { + const initData$ = of([ + { + fruit: 'lemon', + color: 'yellow', + metrics: { inventory: 10, price: 1.2 }, + }, + { + fruit: 'mango', + color: 'orange', + metrics: { inventory: 5, price: 2.5 }, + }, + { + fruit: 'avocado', + color: 'green', + metrics: { inventory: 20, price: 3.0 }, + }, + { + fruit: 'apple', + color: 'red', + metrics: { inventory: 15, price: 1.5 }, + }, + { + fruit: 'orange', + color: 'orange', + metrics: { inventory: 20, price: 1.8 }, + }, + { + fruit: 'banana', + color: 'yellow', + metrics: { inventory: 5, price: 1.0 }, + }, + ]); + const initColumns$ = of( + new TableColumnsBuilder() + .addColumn( + (column) => + column + .label(ColumnNames.fruit) + .displayKey(FruitInfo.fruit) + .ascendingSortFunction((a, b) => a.fruit.localeCompare(b.fruit)) + .sortOrder(1) + .sortable(true) + .sortDirection('asc') // initial sort direction + ) + .addColumn((column) => + column + .displayKey(FruitInfo.colorName) + .label(ColumnNames.colorName) + .sortable(true) + .ascendingSortFunction((a, b) => a.color.localeCompare(b.color)) + ) + .addColumn((column) => + column + .displayKey(FruitInfo.inventory) + .label(ColumnNames.inventory) + .sortable(true) + .getSortValue((x) => x.metrics.inventory) + ) + .getConfig() + ); + this.dataSource = new HsiUiTableDataSource(initData$, initColumns$); + } +} +``` + +```html +@if (dataSource.columns$ | async; as columns) { + + @for (column of columns; track column.id) { + + @if (column.sortable) { + + } @else { + + } + + + } + + +
+
+ {{ column.label }} + {{ sortIcon }} +
{{ column.label }} + {{ element | getValueByKey: column.key }} +
+} +``` + +## Features + +## Configuration + +To provide a column configuration for the `HsiUiTableDataSource`, you can use the +`TableColumnsBuilder`. + +**Required imports from @hsi/ui-components** + +```ts +import { + HsiUiTableDataSource, + TableColumn, + TableColumnsBuilder, + TableModule, +} from '@hsi/ui-components'; +... +@Component({ + ... + imports: [ + TableModule + ... + ], + ... +}) +``` + +**Minimal example of creating a `HsiUiTableDataSource`** + +```ts +... +dataSource: HsiUiTableDataSource<>; +data$: Observable = of([ + { fruit: 'lemon', color: 'yellow' }, + { fruit: 'mango', color: 'orange' }, + ]); +columns$: Observable[];> = of( + new TableColumnsBuilder<{ fruit: string; color: string }>() + .addColumn((column) => + column + .label(ColumnNames.fruit) + ) + .addColumn((column) => + column + .label(ColumnNames.color) + ) + .getConfig()); +... +this.dataSource = new HsiUiTableDataSource(this.data$, this.columns$); +``` + +### Handling tiebreaks + +Tiebreaks can be handled through setting a `sortOrder` on the `TableColumn` objects. When data in +cells of the column currently being sorted are of the same sort value, cell data from the inactive +columns are then compared to determine an ordering of the rows. + +### `TableColumnsBuilder` Methods + +#### Required Methods + +There are no required methods for `TableColumnsBuilder`. + +#### Optional Methods + +The `TableColumm` class contains data that states whether or not the column is sortable, and, if so, +how to sort the column. + +The following methods can be called on `TableColumnsBuilder` to create a list of validated +`TableColumn`s. + +```builder-method +name: addColumn +description: 'Specifies a new column to be added to the end of the list of columns.' +params: + - name: applyToColumn + type: '(column: TableColumnBuilder) => TableColumnBuilder' + description: + - 'Table column configuration for the column to be added, with all desired builder methods applied. See required methods below in the `TableColumnBuilder` section.' +``` + +```builder-method +name: getConfig +description: 'Validates and builds the configuration object for the table columns that can be passed to `HsiUiTableDataSource`.' +params: +- '' +``` + +### `TableColumnBuilder` Methods + +#### Required Methods + +```builder-method +name: id +description: 'Sets the id of the table column.' +params: + - name: id + type: string + description: + - 'The assigned id of the table column.' +``` + +```builder-method +name: displayKey +description: 'Sets the property key in the datum that is to be displayed in the table column. See the `metrics.cost` display key is set in the example below.' +params: + - name: key + type: string + description: + - 'The display key of the table column. Nested object data can be accessed using dot notation (e.g. `metrics.cost`, `metrics.inventory`).' +``` + +```ts +// Datum +fruits = [{ + fruit: 'apple', + color: 'green', + metrics: { + cost: 21, + quantity: 2 + }, + ... +}] + +// Builder +builder = new TableColumnsBuilder<{ +fruit: string; +color: string; +metrics: { + inventory: number; + price: number; +} +}>() +... + .addColumn( + (column) => + column + .label('Cost') + .displayKey('metrics.cost') + ) + ... +``` + +#### Optional Methods + +```builder-method +name: label +description: 'Sets the label of the table column. Useful for storing the desired header text for the column.' +params: + - name: label + type: string + description: + - 'The label to be used by the table column' +``` + +```builder-method +name: getSortValue +description: 'Specifies how to extract the datum property to be sorted on for cells in the table column.' +params: + - name: getSortValue + type: '(x: Datum) => TableValue | null' + description: + - 'A function to extract the datum property to be sorted on for cells in the table column, or `null` to not set this property.' +``` + +```builder-method +name: ascendingSortFunction +description: 'Specifies how datum are to be sorted in ascending order for the table column. If not provided, the column will use `d3.ascending` on the getSortValue output.' +params: + - name: ascendingSortFunction + type: '(a: Datum, b: Datum) => number | null' + description: + - 'The function that sorts datum in ascending order for the table column.' +``` + +```builder-method +name: sortDirection +description: Sets the direction the table column is sorted in. +params: + - name: sortDirection + type: SortDirectionType + description: + - 'The sort direction of the table column.' +``` + +```builder-method +name: sortable +description: 'Sets whether or not the table column can be sorted.' +params: + - name: sortable + type: boolean + description: + - 'Whether the column can be sorted.' +``` + +```builder-method +name: sortOrder +description: 'Sets the order in which the table column is to be sorted by in the case of tiebreaks' +params: + - name: sortOrder + type: number + description: + - 'The sort order of the table column.' +``` + +### Accessing data values using `GetValueByKeyPipe` + +In order to access data from the `Datum` objects in HTML, import the `GetValueByKeyPipe` from the +`AppDevKitModule`. + +```ts +import { GetValueByKeyPipe } from '@hsi/app-dev-kit'; +... +@Component({ + selector: 'app-table-example', + standalone: true, + imports: [CommonModule, TableModule, GetValueByKeyPipe], + templateUrl: './table-example.component.html', + styleUrls: ['../../../examples.scss', './table-example.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +... +``` + +The appropriate display key should then be passed to the pipe in the HTML table implementation like +so: + +```html + + ... + + ... +
{{ element | getValueByKey: column.key }}
+``` + +## Customizing with icons + +Icons of the user's choice can also be included like so: + +In the `th` element: + +```html + + {{ sortIcon }} + {{ column.label }} +``` + +Example CSS code for styling icons in a table: + +```scss +@use 'vars' as *; +@use 'functions' as *; +@use 'colors'; +$icon-width: 0.9rem; +$icon-left-margin: 0.2rem; +$icon-right-margin: 0.4rem; + +.header-cell-sort { + display: flex; + align-items: flex-end; + justify-content: flex-end; + &:hover { + cursor: pointer; + } +} + +.material-symbols-outlined { + display: flex; + justify-content: center; + width: $icon-width; + height: 1.2rem; + font-size: 1.25rem; + margin-left: $icon-left-margin; + margin-right: $icon-right-margin; + opacity: 0.25; + transition: all 150ms ease-in-out; + + &:hover { + opacity: 0.75; + } + + &.actively-sorted { + opacity: 1; + } +} + +.desc { + transform: rotate(180deg); +} + +.table-cell { + text-align: right; + + &.sorted-cell { + padding-right: $icon-left-margin + $icon-width + $icon-right-margin; + } +} + +.table-container { + border-spacing: 0; + td:last-child { + &.left { + padding-right: 0; + } + } + + th:last-child { + &.left { + padding-right: 0; + } + } + + th { + vertical-align: bottom; + &.sorted-header { + padding-right: 0; + } + } +} +``` diff --git a/apps/demo-app/src/assets/ui-components/content/table.md b/apps/demo-app/src/assets/ui-components/content/table.md index 2db81316c..e69de29bb 100644 --- a/apps/demo-app/src/assets/ui-components/content/table.md +++ b/apps/demo-app/src/assets/ui-components/content/table.md @@ -1,513 +0,0 @@ -# Table Component - -## TANSTACK EXAMPLE - -```custom-angular -simple table -``` - -HSI UI Components provides a set of Angular components and configuration classes that can be used to -create tables. These components can be imported via the `TableModule`. - -The HSI UI Components table uses the -[Angular Material CDK Table](https://material.angular.io/cdk/table/overview) as a reference. - -## Composing a Table - -A table is minimally composed of the following HTML components: - -- `table` — A component that is an outer wrapper for other components in the table. -- `th` — A component that represents a header cell in the table. -- `td` — A component that represents a data cell in the table. -- `tr` — A component that represents a row of cells in the table. - -In addition, the table must also be given data through an `HsiUiTableDataSource` instance. - -The following is a minimal implementation: - -```custom-angular -simple table -``` - -```ts -import { CommonModule } from '@angular/common'; -import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core'; -import { GetValueByKeyPipe } from '@hsi/app-dev-kit'; -import { HsiUiTableDataSource, TableColumnsBuilder, TableModule } from '@hsi/ui-components'; -import { of } from 'rxjs'; - -enum ColumnNames { - fruit = 'Fruit', - colorName = 'Color', - inventory = 'Inventory', - price = 'Sell price', -} - -enum FruitInfo { - fruit = 'fruit', - colorName = 'color', - inventory = 'metrics.inventory', - price = 'metrics.price', -} - -class FruitType { - fruit: string; - color: string; - metrics: { - inventory: number; - price: number; - }; -} - -@Component({ - selector: 'app-table-example', - standalone: true, - imports: [CommonModule, TableModule, GetValueByKeyPipe], - templateUrl: './table-example.component.html', - styleUrls: ['../../../examples.scss', './table-example.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class TableExampleComponent implements OnInit { - @Input() sortIcon: string = 'arrow_upward'; - dataSource: HsiUiTableDataSource; - - ngOnInit(): void { - this.setTableData(); - } - - setTableData() { - const initData$ = of([ - { - fruit: 'lemon', - color: 'yellow', - metrics: { inventory: 10, price: 1.2 }, - }, - { - fruit: 'mango', - color: 'orange', - metrics: { inventory: 5, price: 2.5 }, - }, - { - fruit: 'avocado', - color: 'green', - metrics: { inventory: 20, price: 3.0 }, - }, - { - fruit: 'apple', - color: 'red', - metrics: { inventory: 15, price: 1.5 }, - }, - { - fruit: 'orange', - color: 'orange', - metrics: { inventory: 20, price: 1.8 }, - }, - { - fruit: 'banana', - color: 'yellow', - metrics: { inventory: 5, price: 1.0 }, - }, - ]); - const initColumns$ = of( - new TableColumnsBuilder() - .addColumn( - (column) => - column - .label(ColumnNames.fruit) - .displayKey(FruitInfo.fruit) - .ascendingSortFunction((a, b) => a.fruit.localeCompare(b.fruit)) - .sortOrder(1) - .sortable(true) - .sortDirection('asc') // initial sort direction - ) - .addColumn((column) => - column - .displayKey(FruitInfo.colorName) - .label(ColumnNames.colorName) - .sortable(true) - .ascendingSortFunction((a, b) => a.color.localeCompare(b.color)) - ) - .addColumn((column) => - column - .displayKey(FruitInfo.inventory) - .label(ColumnNames.inventory) - .sortable(true) - .getSortValue((x) => x.metrics.inventory) - ) - .getConfig() - ); - this.dataSource = new HsiUiTableDataSource(initData$, initColumns$); - } -} -``` - -```html -@if (dataSource.columns$ | async; as columns) { - - @for (column of columns; track column.id) { - - @if (column.sortable) { - - } @else { - - } - - - } - - -
-
- {{ column.label }} - {{ sortIcon }} -
{{ column.label }} - {{ element | getValueByKey: column.key }} -
-} -``` - -## Features - -## Configuration - -To provide a column configuration for the `HsiUiTableDataSource`, you can use the -`TableColumnsBuilder`. - -**Required imports from @hsi/ui-components** - -```ts -import { - HsiUiTableDataSource, - TableColumn, - TableColumnsBuilder, - TableModule, -} from '@hsi/ui-components'; -... -@Component({ - ... - imports: [ - TableModule - ... - ], - ... -}) -``` - -**Minimal example of creating a `HsiUiTableDataSource`** - -```ts -... -dataSource: HsiUiTableDataSource<>; -data$: Observable = of([ - { fruit: 'lemon', color: 'yellow' }, - { fruit: 'mango', color: 'orange' }, - ]); -columns$: Observable[];> = of( - new TableColumnsBuilder<{ fruit: string; color: string }>() - .addColumn((column) => - column - .label(ColumnNames.fruit) - ) - .addColumn((column) => - column - .label(ColumnNames.color) - ) - .getConfig()); -... -this.dataSource = new HsiUiTableDataSource(this.data$, this.columns$); -``` - -### Handling tiebreaks - -Tiebreaks can be handled through setting a `sortOrder` on the `TableColumn` objects. When data in -cells of the column currently being sorted are of the same sort value, cell data from the inactive -columns are then compared to determine an ordering of the rows. - -### `TableColumnsBuilder` Methods - -#### Required Methods - -There are no required methods for `TableColumnsBuilder`. - -#### Optional Methods - -The `TableColumm` class contains data that states whether or not the column is sortable, and, if so, -how to sort the column. - -The following methods can be called on `TableColumnsBuilder` to create a list of validated -`TableColumn`s. - -```builder-method -name: addColumn -description: 'Specifies a new column to be added to the end of the list of columns.' -params: - - name: applyToColumn - type: '(column: TableColumnBuilder) => TableColumnBuilder' - description: - - 'Table column configuration for the column to be added, with all desired builder methods applied. See required methods below in the `TableColumnBuilder` section.' -``` - -```builder-method -name: getConfig -description: 'Validates and builds the configuration object for the table columns that can be passed to `HsiUiTableDataSource`.' -params: -- '' -``` - -### `TableColumnBuilder` Methods - -#### Required Methods - -```builder-method -name: id -description: 'Sets the id of the table column.' -params: - - name: id - type: string - description: - - 'The assigned id of the table column.' -``` - -```builder-method -name: displayKey -description: 'Sets the property key in the datum that is to be displayed in the table column. See the `metrics.cost` display key is set in the example below.' -params: - - name: key - type: string - description: - - 'The display key of the table column. Nested object data can be accessed using dot notation (e.g. `metrics.cost`, `metrics.inventory`).' -``` - -```ts -// Datum -fruits = [{ - fruit: 'apple', - color: 'green', - metrics: { - cost: 21, - quantity: 2 - }, - ... -}] - -// Builder -builder = new TableColumnsBuilder<{ -fruit: string; -color: string; -metrics: { - inventory: number; - price: number; -} -}>() -... - .addColumn( - (column) => - column - .label('Cost') - .displayKey('metrics.cost') - ) - ... -``` - -#### Optional Methods - -```builder-method -name: label -description: 'Sets the label of the table column. Useful for storing the desired header text for the column.' -params: - - name: label - type: string - description: - - 'The label to be used by the table column' -``` - -```builder-method -name: getSortValue -description: 'Specifies how to extract the datum property to be sorted on for cells in the table column.' -params: - - name: getSortValue - type: '(x: Datum) => TableValue | null' - description: - - 'A function to extract the datum property to be sorted on for cells in the table column, or `null` to not set this property.' -``` - -```builder-method -name: ascendingSortFunction -description: 'Specifies how datum are to be sorted in ascending order for the table column. If not provided, the column will use `d3.ascending` on the getSortValue output.' -params: - - name: ascendingSortFunction - type: '(a: Datum, b: Datum) => number | null' - description: - - 'The function that sorts datum in ascending order for the table column.' -``` - -```builder-method -name: sortDirection -description: Sets the direction the table column is sorted in. -params: - - name: sortDirection - type: SortDirectionType - description: - - 'The sort direction of the table column.' -``` - -```builder-method -name: sortable -description: 'Sets whether or not the table column can be sorted.' -params: - - name: sortable - type: boolean - description: - - 'Whether the column can be sorted.' -``` - -```builder-method -name: sortOrder -description: 'Sets the order in which the table column is to be sorted by in the case of tiebreaks' -params: - - name: sortOrder - type: number - description: - - 'The sort order of the table column.' -``` - -### Accessing data values using `GetValueByKeyPipe` - -In order to access data from the `Datum` objects in HTML, import the `GetValueByKeyPipe` from the -`AppDevKitModule`. - -```ts -import { GetValueByKeyPipe } from '@hsi/app-dev-kit'; -... -@Component({ - selector: 'app-table-example', - standalone: true, - imports: [CommonModule, TableModule, GetValueByKeyPipe], - templateUrl: './table-example.component.html', - styleUrls: ['../../../examples.scss', './table-example.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush, -}) -... -``` - -The appropriate display key should then be passed to the pipe in the HTML table implementation like -so: - -```html - - ... - - ... -
{{ element | getValueByKey: column.key }}
-``` - -## Customizing with icons - -Icons of the user's choice can also be included like so: - -In the `th` element: - -```html - - {{ sortIcon }} - {{ column.label }} -``` - -Example CSS code for styling icons in a table: - -```scss -@use 'vars' as *; -@use 'functions' as *; -@use 'colors'; -$icon-width: 0.9rem; -$icon-left-margin: 0.2rem; -$icon-right-margin: 0.4rem; - -.header-cell-sort { - display: flex; - align-items: flex-end; - justify-content: flex-end; - &:hover { - cursor: pointer; - } -} - -.material-symbols-outlined { - display: flex; - justify-content: center; - width: $icon-width; - height: 1.2rem; - font-size: 1.25rem; - margin-left: $icon-left-margin; - margin-right: $icon-right-margin; - opacity: 0.25; - transition: all 150ms ease-in-out; - - &:hover { - opacity: 0.75; - } - - &.actively-sorted { - opacity: 1; - } -} - -.desc { - transform: rotate(180deg); -} - -.table-cell { - text-align: right; - - &.sorted-cell { - padding-right: $icon-left-margin + $icon-width + $icon-right-margin; - } -} - -.table-container { - border-spacing: 0; - td:last-child { - &.left { - padding-right: 0; - } - } - - th:last-child { - &.left { - padding-right: 0; - } - } - - th { - vertical-align: bottom; - &.sorted-header { - padding-right: 0; - } - } -} -``` diff --git a/libs/app-dev-kit/src/lib/core/pipes/get-value-by-key.pipe.ts b/libs/app-dev-kit/src/lib/core/pipes/get-value-by-key.pipe.ts index ad9cbcc7e..17d2902fb 100644 --- a/libs/app-dev-kit/src/lib/core/pipes/get-value-by-key.pipe.ts +++ b/libs/app-dev-kit/src/lib/core/pipes/get-value-by-key.pipe.ts @@ -1,6 +1,7 @@ import { Pipe, PipeTransform } from '@angular/core'; /** * Retrieves the key in the object. + * @deprecated This pipe is deprecated. See `tanstack-example.component.ts` for the recommended table implementation. */ @Pipe({ name: 'getValueByKey', diff --git a/libs/ui-components/src/lib/table/table-column.ts b/libs/ui-components/src/lib/table/table-column.ts index 1011e0051..5eb470cb0 100644 --- a/libs/ui-components/src/lib/table/table-column.ts +++ b/libs/ui-components/src/lib/table/table-column.ts @@ -9,6 +9,9 @@ export enum SortDirection { export type SortDirectionType = keyof typeof SortDirection; export type TableValue = string | number | boolean | Date; +/** + * @deprecated This class is deprecated. See use of `ColumnDef` in `tanstack-example.component.ts` for the recommended implementation. + */ export class TableColumn { /** * The unique id of the column. diff --git a/libs/ui-components/src/lib/table/table.data-source.ts b/libs/ui-components/src/lib/table/table.data-source.ts index 42145dec7..102087e6f 100644 --- a/libs/ui-components/src/lib/table/table.data-source.ts +++ b/libs/ui-components/src/lib/table/table.data-source.ts @@ -12,6 +12,9 @@ import { } from 'rxjs'; import { SortDirection, TableColumn } from './table-column'; +/** + * @deprecated This class is deprecated. See `tanstack-example.component.ts` for the recommended implementation. + */ export class HsiUiTableDataSource extends DataSource { private sortedData$: Observable; From f30603042cbba1fda76b330365c7654661a2f961 Mon Sep 17 00:00:00 2001 From: Ally Choung Date: Mon, 21 Jul 2025 16:19:32 -0400 Subject: [PATCH 46/47] docs: add deprecated note to table md --- .../assets/ui-components/content/deprecated-table.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/apps/demo-app/src/assets/ui-components/content/deprecated-table.md b/apps/demo-app/src/assets/ui-components/content/deprecated-table.md index ac7c92ca1..ea7c51ea7 100644 --- a/apps/demo-app/src/assets/ui-components/content/deprecated-table.md +++ b/apps/demo-app/src/assets/ui-components/content/deprecated-table.md @@ -1,4 +1,12 @@ -# Table Component +# Table Component (Deprecated) + +### Deprecation Notice + +We have moved away from providing our own table, preferring to use the Tanstack Table library +instead. Tanstack's headless UI approach allows for styling flexibility while letting us take +advantage of open-source solutions to filtering, pagination, sorting, and more. + +## Overview HSI UI Components provides a set of Angular components and configuration classes that can be used to create tables. These components can be imported via the `TableModule`. From 5fdef61b637376edce02a6e74e18e6989ee607f5 Mon Sep 17 00:00:00 2001 From: Ally Choung Date: Mon, 21 Jul 2025 16:26:08 -0400 Subject: [PATCH 47/47] docs: builders should be deprecated too --- .../src/assets/ui-components/content/deprecated-table.md | 2 +- libs/app-dev-kit/src/lib/core/pipes/get-value-by-key.pipe.ts | 5 ++++- libs/ui-components/src/lib/table/table-column-builder.ts | 1 + libs/ui-components/src/lib/table/table-columns.builder.ts | 1 + 4 files changed, 7 insertions(+), 2 deletions(-) diff --git a/apps/demo-app/src/assets/ui-components/content/deprecated-table.md b/apps/demo-app/src/assets/ui-components/content/deprecated-table.md index ea7c51ea7..501e328be 100644 --- a/apps/demo-app/src/assets/ui-components/content/deprecated-table.md +++ b/apps/demo-app/src/assets/ui-components/content/deprecated-table.md @@ -1,6 +1,6 @@ # Table Component (Deprecated) -### Deprecation Notice +## Deprecation Notice We have moved away from providing our own table, preferring to use the Tanstack Table library instead. Tanstack's headless UI approach allows for styling flexibility while letting us take diff --git a/libs/app-dev-kit/src/lib/core/pipes/get-value-by-key.pipe.ts b/libs/app-dev-kit/src/lib/core/pipes/get-value-by-key.pipe.ts index 17d2902fb..5989ac0da 100644 --- a/libs/app-dev-kit/src/lib/core/pipes/get-value-by-key.pipe.ts +++ b/libs/app-dev-kit/src/lib/core/pipes/get-value-by-key.pipe.ts @@ -1,7 +1,10 @@ import { Pipe, PipeTransform } from '@angular/core'; /** * Retrieves the key in the object. - * @deprecated This pipe is deprecated. See `tanstack-example.component.ts` for the recommended table implementation. + * @deprecated This pipe is deprecated. There is no longer a need for a + * custom key accessor pipe since we are now using Tanstack library + * for our table implementation. See `tanstack-example.component.ts` in `ui-components` for + * the recommended table implementation. */ @Pipe({ name: 'getValueByKey', diff --git a/libs/ui-components/src/lib/table/table-column-builder.ts b/libs/ui-components/src/lib/table/table-column-builder.ts index e378520fa..a7b077012 100644 --- a/libs/ui-components/src/lib/table/table-column-builder.ts +++ b/libs/ui-components/src/lib/table/table-column-builder.ts @@ -14,6 +14,7 @@ const DEFAULT = { /** * An internal builder class for a single table column. + * @deprecated This class is deprecated. See `tanstack-example.component.ts` for the recommended implementation. */ export class TableColumnBuilder { private _id: string; diff --git a/libs/ui-components/src/lib/table/table-columns.builder.ts b/libs/ui-components/src/lib/table/table-columns.builder.ts index 88d8a8db7..342fe973d 100644 --- a/libs/ui-components/src/lib/table/table-columns.builder.ts +++ b/libs/ui-components/src/lib/table/table-columns.builder.ts @@ -3,6 +3,7 @@ import { TableColumnBuilder } from './table-column-builder'; /** * User-facing builder class for a list of table columns. + * @deprecated This class is deprecated. See `tanstack-example.component.ts` for the recommended implementation. */ export class TableColumnsBuilder { private _columnBuilders: TableColumnBuilder[] = [];