Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor/table component #2838

Open
wants to merge 5 commits into
base: feat/table-component
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
4 changes: 4 additions & 0 deletions packages/shared/src/utils/object-utils.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"]);
});
});
13 changes: 10 additions & 3 deletions packages/shared/src/utils/object-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,13 +137,20 @@ export function filterObjectByKeys<T extends Record<string, any>, K extends keyo
return Object.fromEntries(filteredEntries) as Partial<Pick<T, K>>;
}

/** Extract all the unique keys across objects in an array */
export function uniqueObjectArrayKeys(arr: Record<string, any>[]) {
/**
* 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<string, any>[], 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<string, boolean> = {};
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);
}
6 changes: 5 additions & 1 deletion src/app/shared/components/template/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -114,7 +119,6 @@ export const TEMPLATE_COMPONENTS = [
TmplSimpleCheckboxComponent,
TmplSliderComponent,
TmplSubtitleComponent,
TmplTableComponent,
TmplTaskCardComponent,
TmplTaskProgressBarComponent,
TmplTextAreaComponent,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,23 @@
<div class="table-container">
<table mat-table [dataSource]="dataSource" matSort>
<ng-container *ngFor="let column of columnsToDisplay" [matColumnDef]="column">
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{ column }}</th>
<td mat-cell *matCellDef="let element">{{ element[column] }}</td>
</ng-container>

<tr mat-header-row *matHeaderRowDef="columnsToDisplay; sticky: true"></tr>
<tr mat-row *matRowDef="let row; columns: columnsToDisplay"></tr>
@if (params().showSearch) {
<ion-input fill="outline" #filterInput (keyup)="applyFilter(filterInput.value)">
<ion-icon slot="start" name="search"></ion-icon>
</ion-input>
}
<table mat-table [dataSource]="dataSource()" matSort>
<cdk-virtual-scroll-viewport autosize style="height: 100%">
@for (column of columnsToDisplay(); track column) {
<ng-container [matColumnDef]="column">
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{ column }}</th>
<td mat-cell *matCellDef="let element">{{ element[column] }}</td>
</ng-container>
}
<tr mat-header-row *matHeaderRowDef="columnsToDisplay(); sticky: true"></tr>
<tr
mat-row
*matRowDef="let row; columns: columnsToDisplay()"
(click)="handleRowClick(row)"
></tr>
</cdk-virtual-scroll-viewport>
</table>
</div>
Original file line number Diff line number Diff line change
@@ -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);
}
Expand Down
113 changes: 70 additions & 43 deletions src/app/shared/components/template/components/table/table.component.ts
Original file line number Diff line number Diff line change
@@ -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<string[]>([]);

columnsToDisplay = [];
dataSource: MatTableDataSource<any>;
public dataSource = signal<MatTableDataSource<any>>(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),
};
}

Expand All @@ -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" };
}
}
4 changes: 0 additions & 4 deletions src/app/shared/components/template/template.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -30,8 +28,6 @@ import { DEMO_COMPONENTS } from "packages/components/demo";
FormsModule,
IonicModule,
LottieModule,
MatSortModule,
MatTableModule,
NgxExtendedPdfViewerModule,
NouisliderModule,
ReactiveFormsModule,
Expand Down
13 changes: 13 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down
Loading