Skip to content

Commit

Permalink
fix(stark-ui): add support for onClickCallback column properties in…
Browse files Browse the repository at this point in the history
… Table component

Add `cellClicked` output in StarkTableColumnComponent.

ISSUES CLOSED: #1466
  • Loading branch information
SuperITMan committed Dec 18, 2019
1 parent 080f13e commit c5e3847
Show file tree
Hide file tree
Showing 8 changed files with 156 additions and 43 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
</th>
<!-- the column template defined by the user will be displayed here -->
<!-- and it will receive the right context containing the displayedValue and the row data-->
<td mat-cell *matCellDef="let rowItem" [ngClass]="getCellClassNames(rowItem)">
<td mat-cell *matCellDef="let rowItem" [ngClass]="getCellClassNames(rowItem)" (click)="onCellClick($event, rowItem)">
<ng-container
*ngTemplateOutlet="columnTemplate; context: { $implicit: { rowData: rowItem, displayedValue: getDisplayedValue(rowItem) } }"
></ng-container>
Expand Down
24 changes: 24 additions & 0 deletions packages/stark-ui/src/modules/table/components/column.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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<StarkColumnCellClickedOutput>();

/**
* Output that will emit a specific column whenever its filter value has changed
*/
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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

Expand All @@ -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();
Expand Down Expand Up @@ -1327,19 +1328,52 @@ 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();
(<Spy>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"
);
expect(descriptionColumnElement).not.toBeNull();

// 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"
);
expect(idColumnElement).not.toBeNull();

// click on the cell
triggerClick(idColumnElement);
expect(hostComponent.rowClickHandler).toHaveBeenCalledTimes(1);
});
});

describe("rowClick", () => {
beforeEach(() => {
hostComponent.columnProperties = [{ name: "id" }, { name: "description" }];
hostComponent.dummyData = dummyData;
hostComponent.dummyData = DUMMY_DATA;

hostFixture.detectChanges(); // trigger data binding
component.ngAfterViewInit();
Expand All @@ -1363,23 +1397,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
Expand All @@ -1401,8 +1429,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", () => {
Expand All @@ -1417,10 +1445,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");
Expand All @@ -1432,7 +1460,7 @@ describe("TableComponent", () => {

beforeEach(() => {
hostComponent.columnProperties = [{ name: "id" }, { name: "description" }];
hostComponent.dummyData = dummyData;
hostComponent.dummyData = DUMMY_DATA;

hostComponent.selection = new SelectionModel<object>(true);
hostComponent.selection.changed.subscribe(handleChange);
Expand All @@ -1450,7 +1478,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", () => {
Expand All @@ -1466,12 +1494,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 = <any>undefined; // data starts uninitialized
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
);
});
}

Expand All @@ -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);
});
Expand Down Expand Up @@ -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<Pick<StarkTableColumnProperties, "onClickCallback">> {
return !!columnProperties && columnProperties.onClickCallback instanceof Function;
}
}
1 change: 1 addition & 0 deletions packages/stark-ui/src/modules/table/entities.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 => {
this.logger.debug("CELL CLICKED - value:", value, ", row: ", row, ", columnName: ", columnName);
}
},
{
name: "extra",
label: "SHOWCASE.DEMO.TABLE.LABELS.EXTRA_INFO",
Expand All @@ -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"
};

Expand Down
17 changes: 15 additions & 2 deletions showcase/src/assets/examples/table/regular/table.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,24 @@ 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 => {
this.logger.debug("CELL CLICKED - value:", value, ", row: ", row, ", columnName: ", columnName);
}
},
{ name: "extra", label: "Extra info", isFilterable: false, isSortable: false, isVisible: false }
];

public filter: StarkTableFilter = { globalFilterPresent: true, columns: [] };
public filter = {
globalFilterPresent: true,
columns: [
{ columnName: "id", filterPosition: "below" },
{ columnName: "title", filterPosition: "above" }
],
filterPosition: "below"
};

public order: string[] = ["title", "-description", "id"];

Expand Down

0 comments on commit c5e3847

Please sign in to comment.