From 2824c6d7661bf944c25c5fd6188362c2345d04d6 Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Sun, 9 Mar 2025 18:23:00 -0700 Subject: [PATCH 1/5] feat: add angular experimental cdk --- package.json | 1 + yarn.lock | 13 +++++++++++++ 2 files changed, 14 insertions(+) diff --git a/package.json b/package.json index 7826a9e0e..cb591fa49 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "private": true, "dependencies": { "@angular/animations": "^17.2.2", + "@angular/cdk-experimental": "^19.2.2", "@angular/common": "~17.2.2", "@angular/core": "~17.2.2", "@angular/elements": "^17.2.2", diff --git a/yarn.lock b/yarn.lock index 4806927b7..9a57c4939 100644 --- a/yarn.lock +++ b/yarn.lock @@ -442,6 +442,18 @@ __metadata: languageName: node linkType: hard +"@angular/cdk-experimental@npm:^19.2.2": + version: 19.2.2 + resolution: "@angular/cdk-experimental@npm:19.2.2" + dependencies: + tslib: ^2.3.0 + peerDependencies: + "@angular/cdk": 19.2.2 + "@angular/core": ^19.0.0 || ^20.0.0 + checksum: c42e7a607d1f4aafb502915e190212ed47fb567df6c8d047cbed70d08cdaabc64a3f252df6f7298fe2c08ccb1b82699006f9efca937db28ae1833888b5ef876c + languageName: node + linkType: hard + "@angular/cdk@npm:^17": version: 17.3.10 resolution: "@angular/cdk@npm:17.3.10" @@ -16508,6 +16520,7 @@ __metadata: "@angular-eslint/template-parser": 17.2.1 "@angular/animations": ^17.2.2 "@angular/cdk": ^17 + "@angular/cdk-experimental": ^19.2.2 "@angular/cli": ~17.2.1 "@angular/common": ~17.2.2 "@angular/compiler": ~17.2.2 From 1e9004faf23c6bd27dc4d89affea6e827b2b5438 Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Sun, 9 Mar 2025 18:23:40 -0700 Subject: [PATCH 2/5] feat: virtual scroller --- .../components/table/table.component.html | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/src/app/shared/components/template/components/table/table.component.html b/src/app/shared/components/template/components/table/table.component.html index 10ab997e1..73e702811 100644 --- a/src/app/shared/components/template/components/table/table.component.html +++ b/src/app/shared/components/template/components/table/table.component.html @@ -1,11 +1,18 @@
- - - - - - - + + @for (column of columnsToDisplay(); track column) { + + + + + } + + +
{{ column }}{{ element[column] }}
{{ column }}{{ element[column] }}
From 87e549cdf09847d1b7d24a22bcfd3c27d104eae7 Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Sun, 9 Mar 2025 18:36:18 -0700 Subject: [PATCH 3/5] refactor: standalone component --- src/app/shared/components/template/components/index.ts | 6 +++++- src/app/shared/components/template/template.module.ts | 4 ---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/app/shared/components/template/components/index.ts b/src/app/shared/components/template/components/index.ts index 858ff0a73..3529fef5d 100644 --- a/src/app/shared/components/template/components/index.ts +++ b/src/app/shared/components/template/components/index.ts @@ -67,6 +67,11 @@ import { YoutubeComponent } from "./youtube/youtube.component"; import { DEMO_COMPONENT_MAPPING } from "components/demo"; import { PLH_COMPONENT_MAPPING } from "components/plh"; +export const TEMPLATE_STANDALONE_COMPONENTS = [ + // tmpl prefix + TmplTableComponent, +]; + /** All components should be exported as a single array for easy module import */ export const TEMPLATE_COMPONENTS = [ // no prefix @@ -114,7 +119,6 @@ export const TEMPLATE_COMPONENTS = [ TmplSimpleCheckboxComponent, TmplSliderComponent, TmplSubtitleComponent, - TmplTableComponent, TmplTaskCardComponent, TmplTaskProgressBarComponent, TmplTextAreaComponent, diff --git a/src/app/shared/components/template/template.module.ts b/src/app/shared/components/template/template.module.ts index c11a6a07d..964fafc79 100644 --- a/src/app/shared/components/template/template.module.ts +++ b/src/app/shared/components/template/template.module.ts @@ -7,8 +7,6 @@ import { NouisliderModule } from "ng2-nouislider"; import { RouterModule } from "@angular/router"; import { SwiperModule } from "swiper/angular"; import { NgxExtendedPdfViewerModule } from "ngx-extended-pdf-viewer"; -import { MatTableModule } from "@angular/material/table"; -import { MatSortModule } from "@angular/material/sort"; import { SharedPipesModule } from "../../pipes"; import { TooltipDirective } from "../common/directives/tooltip.directive"; @@ -30,8 +28,6 @@ import { DEMO_COMPONENTS } from "packages/components/demo"; FormsModule, IonicModule, LottieModule, - MatSortModule, - MatTableModule, NgxExtendedPdfViewerModule, NouisliderModule, ReactiveFormsModule, From bda76c2f57c949c6518a9cc2ffedd11dc3fc35a7 Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Sun, 9 Mar 2025 19:51:03 -0700 Subject: [PATCH 4/5] feat: uniqueObjectArrayKeys maxdepth --- packages/shared/src/utils/object-utils.spec.ts | 4 ++++ packages/shared/src/utils/object-utils.ts | 13 ++++++++++--- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/packages/shared/src/utils/object-utils.spec.ts b/packages/shared/src/utils/object-utils.spec.ts index 9f6f728bb..15db449f0 100644 --- a/packages/shared/src/utils/object-utils.spec.ts +++ b/packages/shared/src/utils/object-utils.spec.ts @@ -160,4 +160,8 @@ describe("Object Utils", () => { ]); expect(res).toEqual(["a", "b", "c"]); }); + it("uniqueObjectArrayKeys max depth", () => { + const res = uniqueObjectArrayKeys([{ a: 1 }, { b: 2 }, { c: 3 }], 2); + expect(res).toEqual(["a", "b"]); + }); }); diff --git a/packages/shared/src/utils/object-utils.ts b/packages/shared/src/utils/object-utils.ts index 900a98eb9..31326c1ed 100644 --- a/packages/shared/src/utils/object-utils.ts +++ b/packages/shared/src/utils/object-utils.ts @@ -137,13 +137,20 @@ export function filterObjectByKeys, K extends keyo return Object.fromEntries(filteredEntries) as Partial>; } -/** Extract all the unique keys across objects in an array */ -export function uniqueObjectArrayKeys(arr: Record[]) { +/** + * Extract all the unique keys across objects in an array + * @param maxDepth - max number of items to iterate over to check for keys. Default checks all + **/ +export function uniqueObjectArrayKeys(arr: Record[], maxDepth?: number) { + // Loop over max number of array items as defined by maxDepth + const loopArr = maxDepth ? arr.slice(0, Math.min(arr.length, maxDepth)) : arr; + const keyHashmap: Record = {}; - for (const el of arr) { + for (const el of loopArr) { for (const key of Object.keys(el)) { keyHashmap[key] = true; } } + console.log({ keyHashmap }); return Object.keys(keyHashmap); } From 5d462a1a949eda458d26b1a524c4ab84165d249e Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Sun, 9 Mar 2025 21:54:53 -0700 Subject: [PATCH 5/5] refactor: table component --- .../components/table/table.component.html | 7 +- .../components/table/table.component.scss | 4 +- .../components/table/table.component.ts | 113 +++++++++++------- 3 files changed, 77 insertions(+), 47 deletions(-) diff --git a/src/app/shared/components/template/components/table/table.component.html b/src/app/shared/components/template/components/table/table.component.html index 73e702811..2cb786ef1 100644 --- a/src/app/shared/components/template/components/table/table.component.html +++ b/src/app/shared/components/template/components/table/table.component.html @@ -1,5 +1,10 @@
- + @if (params().showSearch) { + + + + } +
@for (column of columnsToDisplay(); track column) { diff --git a/src/app/shared/components/template/components/table/table.component.scss b/src/app/shared/components/template/components/table/table.component.scss index d61e023fa..bd7cc1032 100644 --- a/src/app/shared/components/template/components/table/table.component.scss +++ b/src/app/shared/components/template/components/table/table.component.scss @@ -1,8 +1,6 @@ .table-container { - width: 100%; - max-height: 80vh; + height: 100%; overflow: auto; - tr[mat-header-row] { background-color: var(--ion-background-color); } diff --git a/src/app/shared/components/template/components/table/table.component.ts b/src/app/shared/components/template/components/table/table.component.ts index 58fee8258..2dc66161d 100644 --- a/src/app/shared/components/template/components/table/table.component.ts +++ b/src/app/shared/components/template/components/table/table.component.ts @@ -1,63 +1,83 @@ -import { - AfterViewInit, - ChangeDetectorRef, - Component, - computed, - effect, - OnInit, - ViewChild, -} from "@angular/core"; +import { Component, computed, effect, signal, viewChild, ViewChild } from "@angular/core"; import { TemplateBaseComponent } from "../base"; import { FlowTypes } from "packages/data-models"; -import { getBooleanParamFromTemplateRow } from "src/app/shared/utils"; import { AppDataService } from "src/app/shared/services/data/app-data.service"; -import { MatTableDataSource } from "@angular/material/table"; -import { MatSort } from "@angular/material/sort"; +import { MatTableDataSource, MatTableModule } from "@angular/material/table"; +import { MatSort, MatSortable, MatSortModule } from "@angular/material/sort"; +import { uniqueObjectArrayKeys } from "packages/shared/src/utils/object-utils"; +import { ScrollingModule as ExperimentalScrollingModule } from "@angular/cdk-experimental/scrolling"; +import { IonicModule } from "@ionic/angular"; +import { parseBoolean } from "src/app/shared/utils"; +interface IAuthorParams { + /** Ordered list of columns to display. Default display all columns */ + display_columns?: string; + show_search?: string; + sort_default?: string; +} + +/** @ignore */ interface ITableParams { - /** TEMPLATE PARAMETER: show_id. Display the data lists's ID column in the table. Default false */ - showId: boolean; + displayColumns: string[]; + showSearch: boolean; + sortDefault?: MatSortable; } @Component({ selector: "plh-table", templateUrl: "./table.component.html", styleUrls: ["./table.component.scss"], + standalone: true, + imports: [ExperimentalScrollingModule, IonicModule, MatSortModule, MatTableModule], }) -export class TmplTableComponent extends TemplateBaseComponent implements AfterViewInit { - params = computed(() => this.getParams(this.parameterList())); - dataRows = computed(() => this.getDataRowsFromValue(this.value())); +export class TmplTableComponent extends TemplateBaseComponent { + public params = computed(() => this.mapParams(this.parameterList())); + + public columnsToDisplay = signal([]); - columnsToDisplay = []; - dataSource: MatTableDataSource; + public dataSource = signal>(new MatTableDataSource([])); - @ViewChild(MatSort) sort: MatSort; + private sort = viewChild(MatSort); - constructor( - private appDataService: AppDataService, - private cdr: ChangeDetectorRef - ) { + constructor(private appDataService: AppDataService) { super(); + // When row value and params have been received prepare data and + // table options for rendering effect(async () => { - const data = (await this.dataRows()) || []; - this.dataSource = new MatTableDataSource(data); - this.columnsToDisplay = this.getColumnsToDisplayFromData(data); - if (this.sort) { - this.dataSource.sort = this.sort; + const value = this.value(); + const params = this.params(); + if (params && value) { + const dataRows = await this.getDataRowsFromValue(value); + const displayColumns = params.displayColumns || this.inferDisplayColumns(dataRows); + this.columnsToDisplay.set(displayColumns); + this.loadData(dataRows); } - this.cdr.detectChanges(); }); } - ngAfterViewInit() { - if (this.dataSource) { - this.dataSource.sort = this.sort; + public handleRowClick(row) { + // TODO - add way to trigger action with row reference + } + + public applyFilter(value: string | number) { + this.dataSource().filter = `${value}`.trim().toLowerCase(); + } + + private loadData(data: any[] = []) { + const dataSource = new MatTableDataSource(data); + dataSource.sort = this.sort(); + if (this.params().sortDefault) { + dataSource.sort.sort(this.params().sortDefault); } + this.dataSource.set(dataSource); } - private getParams(authorParams: FlowTypes.TemplateRow["parameter_list"]): ITableParams { + /** Map authoring params to component render params */ + private mapParams(authorParams: IAuthorParams): ITableParams { return { - showId: getBooleanParamFromTemplateRow(this._row, "show_id", false), + displayColumns: authorParams.display_columns?.split(",").map((v) => v.trim()), + showSearch: parseBoolean(authorParams.show_search), + sortDefault: this.parseSortDefault(authorParams.sort_default), }; } @@ -68,15 +88,22 @@ export class TmplTableComponent extends TemplateBaseComponent implements AfterVi return []; } const dataList = await this.appDataService.getSheet("data_list", dataListName); - return dataList?.rows; + return dataList?.rows || []; + } + + /** Iterate over first 20 rows or data and extract any unique column names observed */ + private inferDisplayColumns(data: FlowTypes.Data_listRow[] = []): string[] { + return uniqueObjectArrayKeys(data, 20); } - private getColumnsToDisplayFromData(data: FlowTypes.Data_listRow[]): string[] { - if (!data || !data.length) return []; - // Infer columns from first data row - const columnNames = Object.keys(data[0]); - return this.params().showId - ? columnNames - : columnNames.filter((columnName) => columnName !== "id"); + /** + * Convert `sort_default` authoring param to object for use with mat-table sorting. + * Any named column passed by the author becomes the default sorting column, with a fallback + * to initial row + */ + private parseSortDefault(authorSort?: string): MatSortable { + if (!authorSort) return undefined; + let [id, dir] = authorSort.split(" ").map((v) => v.trim()); + return { id: id || "id", disableClear: false, start: dir === "desc" ? "desc" : "asc" }; } }