From 0139ddac8307a578ee623401fc62416350b95204 Mon Sep 17 00:00:00 2001 From: Alexis Georges Date: Fri, 13 Dec 2019 18:30:01 +0100 Subject: [PATCH] fix(stark-ui): add support for `onClickCallback` column properties in Table component Add `cellClicked` output in StarkTableColumnComponent. ISSUES CLOSED: #1466 --- .../table/components/column.component.html | 2 +- .../table/components/column.component.ts | 24 +++++ .../table/components/table.component.spec.ts | 97 ++++++++++++------- .../table/components/table.component.ts | 27 +++++- .../stark-ui/src/modules/table/entities.ts | 1 + .../column-cell-clicked-output.intf.ts | 19 ++++ .../table-regular/table-regular.component.ts | 13 ++- .../assets/examples/table/regular/table.ts | 8 +- 8 files changed, 149 insertions(+), 42 deletions(-) create mode 100644 packages/stark-ui/src/modules/table/entities/column-cell-clicked-output.intf.ts diff --git a/packages/stark-ui/src/modules/table/components/column.component.html b/packages/stark-ui/src/modules/table/components/column.component.html index e3ec77080e..2fe6c61bd8 100644 --- a/packages/stark-ui/src/modules/table/components/column.component.html +++ b/packages/stark-ui/src/modules/table/components/column.component.html @@ -34,7 +34,7 @@ - + diff --git a/packages/stark-ui/src/modules/table/components/column.component.ts b/packages/stark-ui/src/modules/table/components/column.component.ts index 05076331a9..60845d689d 100644 --- a/packages/stark-ui/src/modules/table/components/column.component.ts +++ b/packages/stark-ui/src/modules/table/components/column.component.ts @@ -21,6 +21,7 @@ import { distinctUntilChanged } from "rxjs/operators"; import isEqual from "lodash-es/isEqual"; import get from "lodash-es/get"; import { + StarkColumnCellClickedOutput, StarkColumnFilterChangedOutput, StarkColumnSortChangedOutput, StarkTableColumnFilter, @@ -184,6 +185,12 @@ export class StarkTableColumnComponent extends AbstractStarkUiComponent implemen @Input() public stickyEnd = false; + /** + * Output that will emit a StarkColumnCellClickedOutput whenever a cell in the column is clicked + */ + @Output() + public readonly cellClicked = new EventEmitter(); + /** * Output that will emit a specific column whenever its filter value has changed */ @@ -264,6 +271,23 @@ export class StarkTableColumnComponent extends AbstractStarkUiComponent implemen return rawValue !== null ? rawValue : undefined; } + /** + * Called whenever a cell of the column is clicked + * @param $event - The handled event + * @param row - The row item + */ + public onCellClick($event: Event, row: object): void { + if (this.cellClicked.observers.length > 0) { + $event.stopPropagation(); + + this.cellClicked.emit({ + value: this.getRawValue(row), + row: row, + columnName: this.name + }); + } + } + /** * Get the final displayed value of the column * @param row - The row item diff --git a/packages/stark-ui/src/modules/table/components/table.component.spec.ts b/packages/stark-ui/src/modules/table/components/table.component.spec.ts index d3f18305a6..e08b518987 100644 --- a/packages/stark-ui/src/modules/table/components/table.component.spec.ts +++ b/packages/stark-ui/src/modules/table/components/table.component.spec.ts @@ -97,6 +97,12 @@ describe("TableComponent", () => { element.dispatchEvent(clickEvent); }; + const DUMMY_DATA: object[] = [ + { id: 1, description: "dummy 1" }, + { id: 2, description: "dummy 2" }, + { id: 3, description: "dummy 3" } + ]; + const getColumnSelector = (columnName: string): string => `.stark-table th.mat-column-${columnName} div div`; const columnSelectSelector = "cdk-column-select"; const rowSelector = "table tbody tr"; @@ -1270,11 +1276,6 @@ describe("TableComponent", () => { }); describe("setStyling", () => { - const dummyData: object[] = [ - { id: 1, description: "dummy 1" }, - { id: 2, description: "dummy 2" }, - { id: 3, description: "dummy 3" } - ]; const returnEvenAndOdd: (row: object, index: number) => string = (_row: object, index: number): string => (index + 1) % 2 === 0 ? "even" : "odd"; // offset index with 1 @@ -1284,7 +1285,7 @@ describe("TableComponent", () => { { name: "id", cellClassName: (value: any): string => (value === 1 ? "one" : "") }, { name: "description", cellClassName: "description-body-cell", headerClassName: "description-header-cell" } ]; - hostComponent.dummyData = dummyData; + hostComponent.dummyData = DUMMY_DATA; hostFixture.detectChanges(); // trigger data binding component.ngAfterViewInit(); @@ -1327,19 +1328,53 @@ describe("TableComponent", () => { }); }); - describe("rowClick", () => { - const dummyData: object[] = [ - { id: 1, description: "dummy 1" }, - { id: 2, description: "dummy 2" }, - { - id: 3, - description: "dummy 3" - } - ]; + describe("cellClick", () => { + const onClickCallbackSpy = createSpy("onClickCallback"); + + beforeEach(() => { + hostComponent.columnProperties = [{ name: "id" }, { name: "description", onClickCallback: onClickCallbackSpy }]; + hostComponent.dummyData = DUMMY_DATA; + hostComponent.rowClickHandler = createSpy("rowClickHandlerSpy", () => undefined); // add empty function so spy can find it + + onClickCallbackSpy.calls.reset(); + (hostComponent.rowClickHandler).calls.reset(); + + hostFixture.detectChanges(); // trigger data binding + component.ngAfterViewInit(); + }); + + it("should trigger 'onClickCallback' property when click on the cell and should not emit on 'rowClicked' when 'onClickCallback' is defined",() => { + const descriptionColumnElement = hostFixture.nativeElement.querySelector( + "table tbody tr td.mat-column-description" + ); + // click on the cell + triggerClick(descriptionColumnElement); + + // We expect "2" due to the check "columnProperties.onClickCallback instanceof Function" in table.component.ts + expect(onClickCallbackSpy).toHaveBeenCalledTimes(2); + expect(onClickCallbackSpy).toHaveBeenCalledWith(DUMMY_DATA[0]["description"], DUMMY_DATA[0], "description"); + expect(hostComponent.rowClickHandler).not.toHaveBeenCalled(); + }); + + it("should not trigger 'onClickCallback' property when click on the cell but should emit on 'rowClicked' when 'onClickCallback' is not defined", () => { + const idColumnElement = hostFixture.nativeElement.querySelector( + "table tbody tr td.mat-column-id" + ); + + // click on the cell + triggerClick(idColumnElement); + expect(hostComponent.rowClickHandler).toHaveBeenCalledTimes(1); + }); + }); + + describe("rowClick", () => { + // const onClickCallbackSpy = createSpy("onClickCallback"); + beforeEach(() => { + // hostComponent.columnProperties = [{ name: "id" }, { name: "description", onClickCallback: onClickCallbackSpy }]; hostComponent.columnProperties = [{ name: "id" }, { name: "description" }]; - hostComponent.dummyData = dummyData; + hostComponent.dummyData = DUMMY_DATA; hostFixture.detectChanges(); // trigger data binding component.ngAfterViewInit(); @@ -1363,23 +1398,17 @@ describe("TableComponent", () => { // listener should be called with the data of the first row expect(hostComponent.rowClickHandler).toHaveBeenCalled(); - expect(hostComponent.rowClickHandler).toHaveBeenCalledWith(dummyData[0]); + expect(hostComponent.rowClickHandler).toHaveBeenCalledWith(DUMMY_DATA[0]); // the row should not have been selected - expect(component.selection.isSelected(dummyData[0])).toBe(false); + expect(component.selection.isSelected(DUMMY_DATA[0])).toBe(false); }); }); describe("selection", () => { - const dummyData: object[] = [ - { id: 1, description: "dummy 1" }, - { id: 2, description: "dummy 2" }, - { id: 3, description: "dummy 3" } - ]; - beforeEach(() => { hostComponent.columnProperties = [{ name: "id" }, { name: "description" }]; - hostComponent.dummyData = dummyData; + hostComponent.dummyData = DUMMY_DATA; hostComponent.rowsSelectable = true; hostFixture.detectChanges(); // trigger data binding @@ -1401,8 +1430,8 @@ describe("TableComponent", () => { hostFixture.detectChanges(); expect(rowElement.classList).toContain("selected"); - expect(component.selectChanged.emit).toHaveBeenCalledWith([dummyData[0]]); - expect(component.selection.isSelected(dummyData[0])).toBe(true); + expect(component.selectChanged.emit).toHaveBeenCalledWith([DUMMY_DATA[0]]); + expect(component.selection.isSelected(DUMMY_DATA[0])).toBe(true); }); it("should select the right rows in the template when selecting them through host 'selection' object", () => { @@ -1417,10 +1446,10 @@ describe("TableComponent", () => { expect(rowsElements[1].classList).not.toContain("selected"); expect(rowsElements[2].classList).not.toContain("selected"); - hostComponent.selection.select(dummyData[1]); + hostComponent.selection.select(DUMMY_DATA[1]); hostFixture.detectChanges(); - expect(component.selection.selected).toEqual([dummyData[1]]); + expect(component.selection.selected).toEqual([DUMMY_DATA[1]]); expect(rowsElements[0].classList).not.toContain("selected"); expect(rowsElements[1].classList).toContain("selected"); expect(rowsElements[2].classList).not.toContain("selected"); @@ -1432,7 +1461,7 @@ describe("TableComponent", () => { beforeEach(() => { hostComponent.columnProperties = [{ name: "id" }, { name: "description" }]; - hostComponent.dummyData = dummyData; + hostComponent.dummyData = DUMMY_DATA; hostComponent.selection = new SelectionModel(true); hostComponent.selection.changed.subscribe(handleChange); @@ -1450,7 +1479,7 @@ describe("TableComponent", () => { selectAllButton.click(); hostFixture.detectChanges(); - expect(handleChange).toHaveBeenCalledTimes(dummyData.length); + expect(handleChange).toHaveBeenCalledTimes(DUMMY_DATA.length); }); it("should select all rows available after filter when clicking the select all", () => { @@ -1466,12 +1495,6 @@ describe("TableComponent", () => { }); describe("async", () => { - const DUMMY_DATA: object[] = [ - { id: 1, description: "dummy 1" }, - { id: 2, description: "dummy 2" }, - { id: 3, description: "dummy 3" } - ]; - beforeEach(() => { hostComponent.columnProperties = [{ name: "id" }, { name: "description" }]; hostComponent.dummyData = undefined; // data starts uninitialized diff --git a/packages/stark-ui/src/modules/table/components/table.component.ts b/packages/stark-ui/src/modules/table/components/table.component.ts index 563d05d55a..de624a610e 100644 --- a/packages/stark-ui/src/modules/table/components/table.component.ts +++ b/packages/stark-ui/src/modules/table/components/table.component.ts @@ -32,6 +32,7 @@ import { StarkTableColumnComponent } from "./column.component"; import { StarkSortingRule, StarkTableMultisortDialogComponent, StarkTableMultisortDialogData } from "./dialogs/multisort.component"; import { StarkAction, StarkActionBarConfig } from "../../action-bar/components"; import { + StarkColumnCellClickedOutput, StarkColumnFilterChangedOutput, StarkColumnSortChangedOutput, StarkTableColumnFilter, @@ -597,7 +598,10 @@ export class StarkTableComponent extends AbstractStarkUiComponent implements OnI oldColumns.forEach((oldColumn: MatColumnDef) => { this.table.removeColumnDef(oldColumn); // removing column also from the displayed columns (such array should match the dataSource!) - this.displayedColumns.splice(this.displayedColumns.findIndex((column: string) => column === oldColumn.name), 1); + this.displayedColumns.splice( + this.displayedColumns.findIndex((column: string) => column === oldColumn.name), + 1 + ); }); } @@ -609,6 +613,17 @@ export class StarkTableComponent extends AbstractStarkUiComponent implements OnI for (const column of columns) { this.table.addColumnDef(column.columnDef); + const columnProperties = find( + this.columnProperties, + (columnProps: StarkTableColumnProperties) => column.name === columnProps.name + ); + + if (this.isColumnPropertiesWithOnClickCallback(columnProperties)) { + column.cellClicked.subscribe((cellClickedOutput: StarkColumnCellClickedOutput) => { + columnProperties.onClickCallback(cellClickedOutput.value, cellClickedOutput.row, cellClickedOutput.columnName); + }); + } + column.sortChanged.subscribe((sortedColumn: StarkColumnSortChangedOutput) => { this.onReorderChange(sortedColumn); }); @@ -1123,4 +1138,14 @@ export class StarkTableComponent extends AbstractStarkUiComponent implements OnI public trackColumnFn(_index: number, item: StarkTableColumnProperties): string { return item.name; } + + /** + * @ignore + * Type guard + */ + private isColumnPropertiesWithOnClickCallback( + columnProperties?: StarkTableColumnProperties + ): columnProperties is StarkTableColumnProperties & Required> { + return !!columnProperties && columnProperties.onClickCallback instanceof Function; + } } diff --git a/packages/stark-ui/src/modules/table/entities.ts b/packages/stark-ui/src/modules/table/entities.ts index 076b7f26a8..a1e1b33aad 100644 --- a/packages/stark-ui/src/modules/table/entities.ts +++ b/packages/stark-ui/src/modules/table/entities.ts @@ -1,3 +1,4 @@ +export * from "./entities/column-cell-clicked-output.intf"; export * from "./entities/column-priority.intf"; export * from "./entities/column-properties.intf"; export * from "./entities/column-sort-changed-output.intf"; diff --git a/packages/stark-ui/src/modules/table/entities/column-cell-clicked-output.intf.ts b/packages/stark-ui/src/modules/table/entities/column-cell-clicked-output.intf.ts new file mode 100644 index 0000000000..0723caa3cd --- /dev/null +++ b/packages/stark-ui/src/modules/table/entities/column-cell-clicked-output.intf.ts @@ -0,0 +1,19 @@ +/** + * Definition of the output value of StarkTableColumn cellClicked Output + */ +export interface StarkColumnCellClickedOutput { + /** + * The value of the cell that was clicked + */ + value: any; + + /** + * The column name that the cell belongs to + */ + columnName: string; + + /** + * The row object that contains the cell that was clicked + */ + row: object; +} diff --git a/showcase/src/app/demo-ui/components/table-regular/table-regular.component.ts b/showcase/src/app/demo-ui/components/table-regular/table-regular.component.ts index 2701d2d03a..5a2d1d0638 100644 --- a/showcase/src/app/demo-ui/components/table-regular/table-regular.component.ts +++ b/showcase/src/app/demo-ui/components/table-regular/table-regular.component.ts @@ -50,7 +50,13 @@ export class TableRegularComponent { cellFormatter: (value: { label: string }): string => "~" + value.label, compareFn: (n1: { value: number }, n2: { value: number }): number => n1.value - n2.value }, - { name: "description", label: "SHOWCASE.DEMO.TABLE.LABELS.DESCRIPTION" }, + { + name: "description", + label: "SHOWCASE.DEMO.TABLE.LABELS.DESCRIPTION", + onClickCallback: (value: any, row?: object, columnName?: string): void => { + console.log("CELL CLICKED - value:", value, ", row: ", row, ", columnName: ", columnName); + } + }, { name: "extra", label: "SHOWCASE.DEMO.TABLE.LABELS.EXTRA_INFO", @@ -62,7 +68,10 @@ export class TableRegularComponent { public filter: StarkTableFilter = { globalFilterPresent: true, - columns: [{ columnName: "id", filterPosition: "below" }, { columnName: "title", filterPosition: "above" }], + columns: [ + { columnName: "id", filterPosition: "below" }, + { columnName: "title", filterPosition: "above" } + ], filterPosition: "below" }; diff --git a/showcase/src/assets/examples/table/regular/table.ts b/showcase/src/assets/examples/table/regular/table.ts index 32f932d9f8..d19109d72b 100644 --- a/showcase/src/assets/examples/table/regular/table.ts +++ b/showcase/src/assets/examples/table/regular/table.ts @@ -30,7 +30,13 @@ export class TableRegularComponent { cellFormatter: (value: { label: string }): string => "~" + value.label, compareFn: (n1: { value: number }, n2: { value: number }): number => n1.value - n2.value }, - { name: "description", label: "Description" }, + { + name: "description", + label: "Description", + onClickCallback: (value: any, row?: object, columnName?: string): void => { + console.log("CELL CLICKED - value:", value, ", row: ", row, ", columnName: ", columnName); + } + }, { name: "extra", label: "Extra info", isFilterable: false, isSortable: false, isVisible: false } ];