Skip to content

Commit

Permalink
feat(selection): add caller property to onSelectedRowsChanged event
Browse files Browse the repository at this point in the history
- for our project use case, we have to select all the children of a parent in a Tree structure, and prior to this PR there was no way of knowing if the `setSelectedRows` was called by a user checkbox click or via dynamically via code. So by adding a `caller` property to the `onSelectedRowsChanged` event, we can now add logic to focus only on user clicks and disregarding anything else.
- also note that there was also a PR created (and merged) on the core SlickGrid lib to go with this PR
  • Loading branch information
ghiscoding committed Dec 7, 2021
1 parent 5aba2a4 commit cc5f4ae
Show file tree
Hide file tree
Showing 11 changed files with 143 additions and 30 deletions.
99 changes: 99 additions & 0 deletions examples/webpack-demo-vanilla-bundle/src/examples/example05.ts
Expand Up @@ -7,6 +7,7 @@ import {
GridOption,
GridStateChange,
GridStateType,
OnSelectedRowsChangedEventArgs,
TreeToggledItem,
TreeToggleStateChange,
} from '@slickgrid-universal/common';
Expand Down Expand Up @@ -58,6 +59,10 @@ export class Example5 {
this._bindingEventService.bind(gridContainerElm, 'ontreeitemtoggled', this.handleOnTreeItemToggled.bind(this));
// or use the Grid State change event
// this._bindingEventService.bind(gridContainerElm, 'ongridstatechanged', this.handleOnGridStateChanged.bind(this));

// the following event is a special use case for our project and is commented out
// so that we still have code ref if we still need to test the use case
// this._bindingEventService.bind(gridContainerElm, 'onselectedrowschanged', this.handleOnSelectedRowsChanged.bind(this));
}

dispose() {
Expand All @@ -74,6 +79,93 @@ export class Example5 {
}
}

/**
* From an item object, we'll first check if item is a parent at level 0 and if so we'll return an array of all of its children Ids (including parent Id itself)
* @param {Object} itemObj - selected item object
* @returns {Array<number>}
*/
getTreeIds(itemObj) {
let treeIds = [];
if (itemObj.__hasChildren && itemObj.treeLevel === 0) {
treeIds = this.sgb.dataset
.filter(item => item.parentId === itemObj.id)
.map(child => child.id);
treeIds.push(itemObj.id); // also add parent Id into the list for the complete tree Ids
}
return treeIds;
}

/**
* From an item object, find how many item(s) are selected in its tree.
* @param {Object} itemObj - selected item object
* @param {Array<number>} - we must provide the selected rows prior to the checkbox toggling, we can get this directly from the `onselectedrowschanged` event
*/
getTreeSelectedCount(rootParentItemObj, previousSelectedRows: number[]) {
let treeIds = [];
const selectedIds = this.sgb.dataView.mapRowsToIds(previousSelectedRows);
if (rootParentItemObj.__hasChildren && rootParentItemObj.treeLevel === 0) {
treeIds = this.sgb.dataset.filter(item => item.parentId === rootParentItemObj.id)
.filter(item => selectedIds.some(selectedId => selectedId === item.id))
.map(child => child.id);
}
return treeIds.length;
}

/** Testing of a use case we had in our environment which is to select all child items of the tree being selected */
handleOnSelectedRowsChanged(event) {
const args = event.detail.args as OnSelectedRowsChangedEventArgs;

if (args.caller === 'click.toggle') {
let treeIds: Array<number> = [];
const childItemsIdxAndIds: Array<{ itemId: number; rowIdx: number; }> = [];
const allSelectionChanges = args.changedSelectedRows.concat(args.changedUnselectedRows);
let changedRowIndex = allSelectionChanges.length > 0 ? allSelectionChanges[0] : null;

if (changedRowIndex !== null) {
const isRowBeingUnselected = (args.changedUnselectedRows.length && changedRowIndex === args.changedUnselectedRows[0]);
let selectedRowItemObj = this.sgb.dataView.getItem(changedRowIndex);

// the steps we'll do below are the same for both the if/else
// 1. we will find all of its children Ids
// 2. we will toggle all of its underlying child (only on first treeLevel though)

// step 1) if it's a parent item (or none of the tree items are selected) we'll select (or unselect) its entire tree, basically the children must follow what the parent does (selected or unselected)
if (selectedRowItemObj.__hasChildren && selectedRowItemObj.treeLevel === 0) {
// when it's a parent we'll make sure to expand the current tree on first level (if not yet expanded) and then return all tree Ids
this.sgb.treeDataService.dynamicallyToggleItemState([{ itemId: selectedRowItemObj.id, isCollapsed: false }]);
treeIds = this.getTreeIds(selectedRowItemObj);
} else if ((selectedRowItemObj.__hasChildren && selectedRowItemObj.treeLevel === 1) || (!selectedRowItemObj.__hasChildren && selectedRowItemObj.parentId && !this.getTreeSelectedCount(this.sgb.dataView.getItemById(selectedRowItemObj.parentId), args.previousSelectedRows))) {
// if we're toggling a child item that is also a parent item (e.g. a switchboard inside an INEQ) then we'll do the following
// circle back to its root parent and perform (in other word use the parent of this parent)
// then same as previous condition, we'll return the Ids of that that tree
const selectedItem = this.sgb.dataView.getItem(changedRowIndex);
selectedRowItemObj = this.sgb.dataView.getItemById(selectedItem.parentId);
changedRowIndex = this.sgb.dataView.mapIdsToRows([selectedItem.parentId])?.[0];
treeIds = this.getTreeIds(selectedRowItemObj);
}

// step 2) do the toggle select/unselect of that tree when necessary (we only toggle on a parent item)
if (treeIds.length > 0) {
const currentSelectedRows = this.sgb.slickGrid.getSelectedRows();
for (const leafId of treeIds) {
const childIndexes = this.sgb.dataView.mapIdsToRows([leafId]);
if (Array.isArray(childIndexes) && childIndexes.length > 0) {
childItemsIdxAndIds.push({ itemId: leafId, rowIdx: childIndexes[0] });
}
}
const childrenRowIndexes = childItemsIdxAndIds.map(childItem => childItem.rowIdx);
const currentSelectionPlusChildrenIndexes: number[] = Array.from(new Set(currentSelectedRows.concat(childrenRowIndexes))); // use Set to remove duplicates

// if we are unselecting the row then we'll remove the children from the final list of selections else just use that entire tree Ids
const finalSelection = isRowBeingUnselected
? currentSelectionPlusChildrenIndexes.filter(rowIdx => !childrenRowIndexes.includes(rowIdx))
: currentSelectionPlusChildrenIndexes;
this.sgb.slickGrid.setSelectedRows(finalSelection);
}
}
}
}

initializeGrid() {
this.columnDefinitions = [
{
Expand Down Expand Up @@ -130,6 +222,13 @@ export class Example5 {
// optionally display some text on the left footer container
leftFooterText: 'Grid created with <a href="https://github.com/ghiscoding/slickgrid-universal" target="_blank">Slickgrid-Universal</a> <i class="mdi mdi-github"></i>',
},
// enableCheckboxSelector: true,
// enableRowSelection: true,
// multiSelect: false,
// checkboxSelector: {
// hideInFilterHeaderRow: false,
// hideInColumnTitleRow: true,
// },
enableTreeData: true, // you must enable this flag for the filtering & sorting to work as expected
treeDataOptions: {
columnId: 'title',
Expand Down
2 changes: 1 addition & 1 deletion packages/common/package.json
Expand Up @@ -78,7 +78,7 @@
"jquery-ui-dist": "^1.12.1",
"moment-mini": "^2.24.0",
"multiple-select-modified": "^1.3.15",
"slickgrid": "^2.4.43",
"slickgrid": "^2.4.44",
"un-flatten-tree": "^2.0.12"
},
"devDependencies": {
Expand Down
Expand Up @@ -259,6 +259,7 @@ describe('CellSelectionModel Plugin', () => {
jest.spyOn(gridStub, 'canCellBeSelected').mockReturnValue(true);
const scrollRowSpy = jest.spyOn(gridStub, 'scrollRowIntoView');
const scrollCellSpy = jest.spyOn(gridStub, 'scrollCellIntoView');
const onSelectedRangeSpy = jest.spyOn(plugin.onSelectedRangesChanged, 'notify');

plugin.init(gridStub);
plugin.setSelectedRanges([
Expand All @@ -269,15 +270,17 @@ describe('CellSelectionModel Plugin', () => {
const keyDownEvent = addJQueryEventPropagation(new Event('keydown'), 'shiftKey', 'ArrowDown');
gridStub.onKeyDown.notify({ cell: 2, row: 3, grid: gridStub }, keyDownEvent, gridStub);

expect(setSelectRangeSpy).toHaveBeenCalledWith([
const expectedRangeCalled = [
{ fromCell: 1, fromRow: 2, toCell: 3, toRow: 4, contains: expect.toBeFunction(), } as unknown as SlickRange,
{
fromCell: 2, fromRow: 3, toCell: 2, toRow: 4,
contains: expect.toBeFunction(), toString: expect.toBeFunction(), isSingleCell: expect.toBeFunction(), isSingleRow: expect.toBeFunction(),
},
]);
];
expect(setSelectRangeSpy).toHaveBeenCalledWith(expectedRangeCalled);
expect(scrollCellSpy).toHaveBeenCalledWith(4, 2, false);
expect(scrollRowSpy).toHaveBeenCalledWith(4);
expect(onSelectedRangeSpy).toHaveBeenCalledWith(expectedRangeCalled, expect.objectContaining({ detail: { caller: 'SlickCellSelectionModel.setSelectedRanges' } }));
});

it('should call "rangesAreEqual" and expect True when both ranges are equal', () => {
Expand Down
@@ -1,5 +1,5 @@
import { SlickCheckboxSelectColumn } from '../slickCheckboxSelectColumn';
import { Column, SlickGrid, SlickNamespace, } from '../../interfaces/index';
import { Column, OnSelectedRowsChangedEventArgs, SlickGrid, SlickNamespace, } from '../../interfaces/index';
import { SlickRowSelectionModel } from '../../extensions/slickRowSelectionModel';

declare const Slick: SlickNamespace;
Expand Down Expand Up @@ -189,7 +189,7 @@ describe('SlickCheckboxSelectColumn Plugin', () => {
expect(plugin).toBeTruthy();
expect(stopPropagationSpy).toHaveBeenCalled();
expect(stopImmediatePropagationSpy).toHaveBeenCalled();
expect(setSelectedRowSpy).toHaveBeenCalledWith([0, 1, 2]);
expect(setSelectedRowSpy).toHaveBeenCalledWith([0, 1, 2], 'click.selectAll');
});

it('should create the plugin and call "setOptions" and expect options changed and hide both Select All toggle when setting "hideSelectAllCheckbox: false" and "hideInColumnTitleRow: true"', () => {
Expand Down Expand Up @@ -221,7 +221,7 @@ describe('SlickCheckboxSelectColumn Plugin', () => {
plugin.selectRows([1, 2, 3]);
plugin.deSelectRows([1, 2, 3, 6, -1]);

expect(setSelectedRowSpy).toHaveBeenNthCalledWith(2, [1]); // only 1 is found which was previous false
expect(setSelectedRowSpy).toHaveBeenNthCalledWith(2, [1], 'SlickCheckboxSelectColumn.deSelectRows'); // only 1 is found which was previous false
});

it('should pre-select some rows in a delay when "preselectedRows" is defined with a row selection model', (done) => {
Expand Down Expand Up @@ -250,7 +250,7 @@ describe('SlickCheckboxSelectColumn Plugin', () => {
plugin.toggleRowSelection(2);

expect(setActiveCellSpy).toHaveBeenCalledWith(2, 0);
expect(setSelectedRowSpy).toHaveBeenNthCalledWith(2, [1, 2, 2]);
expect(setSelectedRowSpy).toHaveBeenNthCalledWith(2, [1, 2, 2], 'click.toggle');
});

it('should call "toggleRowSelection" and expect "setActiveCell" not being called when the selectableOverride is returning false', () => {
Expand Down Expand Up @@ -279,7 +279,7 @@ describe('SlickCheckboxSelectColumn Plugin', () => {
plugin.selectRows([2, 3]);
plugin.toggleRowSelection(2);

expect(setSelectedRowSpy).toHaveBeenNthCalledWith(2, [1]);
expect(setSelectedRowSpy).toHaveBeenNthCalledWith(2, [1], 'click.toggle');
expect(setActiveCellSpy).toHaveBeenCalledWith(2, 0);
});

Expand Down Expand Up @@ -336,7 +336,7 @@ describe('SlickCheckboxSelectColumn Plugin', () => {
inputCheckboxElm.dispatchEvent(new Event('click', { bubbles: true, cancelable: true }));

expect(inputCheckboxElm).toBeTruthy();
expect(setSelectedRowSpy).toHaveBeenCalledWith([]);
expect(setSelectedRowSpy).toHaveBeenCalledWith([], 'click.selectAll');
});

it('should call the "create" method and expect plugin to be created with checkbox column to be created at position 0 when using default', () => {
Expand Down Expand Up @@ -488,7 +488,7 @@ describe('SlickCheckboxSelectColumn Plugin', () => {
const checkboxElm = document.createElement('input');
checkboxElm.type = 'checkbox';
const clickEvent = addJQueryEventPropagation(new Event('keyDown'), '', ' ', checkboxElm);
gridStub.onSelectedRowsChanged.notify({ rows: [2, 3], previousSelectedRows: [0, 1], grid: gridStub }, clickEvent);
gridStub.onSelectedRowsChanged.notify({ rows: [2, 3], previousSelectedRows: [0, 1], grid: gridStub } as OnSelectedRowsChangedEventArgs, clickEvent);

expect(plugin).toBeTruthy();
expect(invalidateRowSpy).toHaveBeenCalled();
Expand Down Expand Up @@ -519,7 +519,7 @@ describe('SlickCheckboxSelectColumn Plugin', () => {
const checkboxElm = document.createElement('input');
checkboxElm.type = 'checkbox';
const clickEvent = addJQueryEventPropagation(new Event('keyDown'), '', ' ', checkboxElm);
gridStub.onSelectedRowsChanged.notify({ rows: [2, 3], previousSelectedRows: [0, 1], grid: gridStub }, clickEvent);
gridStub.onSelectedRowsChanged.notify({ rows: [2, 3], previousSelectedRows: [0, 1], grid: gridStub } as OnSelectedRowsChangedEventArgs, clickEvent);

expect(plugin).toBeTruthy();
expect(invalidateRowSpy).toHaveBeenCalled();
Expand Down
Expand Up @@ -140,7 +140,7 @@ describe('SlickRowSelectionModel Plugin', () => {
fromCell: 0, fromRow: 2, toCell: 2, toRow: 2,
contains: expect.toBeFunction(), toString: expect.toBeFunction(), isSingleCell: expect.toBeFunction(), isSingleRow: expect.toBeFunction(),
}];
expect(setSelectedRangeSpy).toHaveBeenCalledWith(expectedRanges);
expect(setSelectedRangeSpy).toHaveBeenCalledWith(expectedRanges, 'SlickRowSelectionModel.setSelectedRows');
expect(plugin.getSelectedRanges()).toEqual(expectedRanges);
expect(plugin.getSelectedRows()).toEqual([0, 2]);
});
Expand All @@ -153,8 +153,14 @@ describe('SlickRowSelectionModel Plugin', () => {

it('should call "setSelectedRanges" with valid ranges input and expect to "onSelectedRangesChanged" to be triggered', () => {
const onSelectedRangeSpy = jest.spyOn(plugin.onSelectedRangesChanged, 'notify');

plugin.setSelectedRanges([{ fromCell: 0, fromRow: 0, toCell: 2, toRow: 0, }]);
expect(onSelectedRangeSpy).toHaveBeenCalledWith([{ fromCell: 0, fromRow: 0, toCell: 2, toRow: 0, }]);

expect(onSelectedRangeSpy).toHaveBeenCalledWith(
[{ fromCell: 0, fromRow: 0, toCell: 2, toRow: 0, }],
expect.objectContaining({
detail: { caller: 'SlickRowSelectionModel.setSelectedRanges' }
}));
});

it('should call "setSelectedRanges" with Slick Ranges when triggered by "onActiveCellChanged" and "selectActiveRow" is True', () => {
Expand Down
6 changes: 4 additions & 2 deletions packages/common/src/extensions/slickCellSelectionModel.ts
Expand Up @@ -103,7 +103,7 @@ export class SlickCellSelectionModel {
return result;
}

setSelectedRanges(ranges: CellRange[]) {
setSelectedRanges(ranges: CellRange[], caller = 'SlickCellSelectionModel.setSelectedRanges') {
// simple check for: empty selection didn't change, prevent firing onSelectedRangesChanged
if ((!this._ranges || this._ranges.length === 0) && (!ranges || ranges.length === 0)) {
return;
Expand All @@ -114,7 +114,9 @@ export class SlickCellSelectionModel {

this._ranges = this.removeInvalidRanges(ranges);
if (rangeHasChanged) {
this.onSelectedRangesChanged.notify(this._ranges);
const eventData = new Slick.EventData();
Object.defineProperty(eventData, 'detail', { writable: true, configurable: true, value: { caller } });
this.onSelectedRangesChanged.notify(this._ranges, eventData);
}
}

Expand Down
10 changes: 5 additions & 5 deletions packages/common/src/extensions/slickCheckboxSelectColumn.ts
Expand Up @@ -155,7 +155,7 @@ export class SlickCheckboxSelectColumn<T = any> {
removeRows[removeRows.length] = row;
}
}
this._grid.setSelectedRows(this._grid.getSelectedRows().filter((n) => removeRows.indexOf(n) < 0));
this._grid.setSelectedRows(this._grid.getSelectedRows().filter((n) => removeRows.indexOf(n) < 0), 'SlickCheckboxSelectColumn.deSelectRows');
}

selectRows(rowArray: number[]) {
Expand Down Expand Up @@ -207,7 +207,7 @@ export class SlickCheckboxSelectColumn<T = any> {
}

const newSelectedRows = this._selectedRowsLookup[row] ? this._grid.getSelectedRows().filter((n) => n !== row) : this._grid.getSelectedRows().concat(row);
this._grid.setSelectedRows(newSelectedRows);
this._grid.setSelectedRows(newSelectedRows, 'click.toggle');
this._grid.setActiveCell(row, this.getCheckboxColumnCellIndex());
}

Expand Down Expand Up @@ -312,9 +312,9 @@ export class SlickCheckboxSelectColumn<T = any> {
rows.push(i);
}
}
this._grid.setSelectedRows(rows);
this._grid.setSelectedRows(rows, 'click.selectAll');
} else {
this._grid.setSelectedRows([]);
this._grid.setSelectedRows([], 'click.selectAll');
}
e.stopPropagation();
e.stopImmediatePropagation();
Expand Down Expand Up @@ -392,7 +392,7 @@ export class SlickCheckboxSelectColumn<T = any> {
const remIdx = selectedRows.indexOf(itemToRemove);
selectedRows.splice(remIdx, 1);
}
this._grid.setSelectedRows(selectedRows);
this._grid.setSelectedRows(selectedRows, 'click.toggle');
}
}

Expand Down
8 changes: 5 additions & 3 deletions packages/common/src/extensions/slickRowSelectionModel.ts
Expand Up @@ -54,16 +54,18 @@ export class SlickRowSelectionModel {
}

setSelectedRows(rows: number[]) {
this.setSelectedRanges(this.rowsToRanges(rows));
this.setSelectedRanges(this.rowsToRanges(rows), 'SlickRowSelectionModel.setSelectedRows');
}

setSelectedRanges(ranges: CellRange[]) {
setSelectedRanges(ranges: CellRange[], caller = 'SlickRowSelectionModel.setSelectedRanges') {
// simple check for: empty selection didn't change, prevent firing onSelectedRangesChanged
if ((!this._ranges || this._ranges.length === 0) && (!ranges || ranges.length === 0)) {
return;
}
this._ranges = ranges;
this.onSelectedRangesChanged.notify(this._ranges);
const eventData = new Slick.EventData();
Object.defineProperty(eventData, 'detail', { writable: true, configurable: true, value: { caller } });
this.onSelectedRangesChanged.notify(this._ranges, eventData);
}

//
Expand Down
7 changes: 4 additions & 3 deletions packages/common/src/interfaces/slickGrid.interface.ts
Expand Up @@ -435,9 +435,10 @@ export interface SlickGrid {

/**
* Accepts an array of row indices and applies the current selectedCellCssClass to the cells in the row, respecting whether cells have been flagged as selectable.
* @param rowsArray An array of row numbers.
* @param {Array<number>} rowsArray - an array of row numbers.
* @param {String} caller - an optional string to identify who called the method
*/
setSelectedRows(rowsArray: number[]): void;
setSelectedRows(rowsArray: number[], caller?: string): void;

/**
* Unregisters a current selection model and registers a new one. See the definition of SelectionModel for more information.
Expand Down Expand Up @@ -571,7 +572,7 @@ export interface OnHeaderRowCellRenderedEventArgs extends SlickGridEventData { n
export interface OnKeyDownEventArgs extends SlickGridEventData { row: number; cell: number; }
export interface OnValidationErrorEventArgs extends SlickGridEventData { row: number; cell: number; validationResults: EditorValidationResult; column: Column; editor: Editor; cellNode: HTMLDivElement; }
export interface OnRenderedEventArgs extends SlickGridEventData { startRow: number; endRow: number; }
export interface OnSelectedRowsChangedEventArgs extends SlickGridEventData { rows: number[]; previousSelectedRows: number[]; }
export interface OnSelectedRowsChangedEventArgs extends SlickGridEventData { rows: number[]; previousSelectedRows: number[]; changedSelectedRows: number[]; changedUnselectedRows: number[]; caller: string; }
export interface OnSetOptionsEventArgs extends SlickGridEventData { optionsBefore: GridOption; optionsAfter: GridOption; }

export interface OnScrollEventArgs extends SlickGridEventData { scrollLeft: number; scrollTop: number; }
Expand Down
Binary file not shown.
8 changes: 4 additions & 4 deletions yarn.lock
Expand Up @@ -10781,10 +10781,10 @@ slice-ansi@^4.0.0:
astral-regex "^2.0.0"
is-fullwidth-code-point "^3.0.0"

slickgrid@^2.4.43:
version "2.4.43"
resolved "https://registry.yarnpkg.com/slickgrid/-/slickgrid-2.4.43.tgz#c646406dcea0c913fdf56a1bf60ba8d3103c0726"
integrity sha512-xq/2eJK4s+s99EWDBR71EX/i1tu8oSrtvmnEXPQ1TRtApVhtPfrooJ34pnkm6khaAAA1mvNSLpmhsPCzaxNmxg==
slickgrid@^2.4.44:
version "2.4.44"
resolved "https://registry.yarnpkg.com/slickgrid/-/slickgrid-2.4.44.tgz#ddcb41c8a0184a2c177bc88d90e428ff42e1cb9a"
integrity sha512-DF7ePE5bwitJrRdJSNrV+qAnQsfds0GbRA02ywy6TQrQewkm9DSHGDUxJaoJk2WUMlyQ7Odrf2o1PCZM50BcSg==
dependencies:
jquery ">=1.8.0"
jquery-ui ">=1.8.0"
Expand Down

0 comments on commit cc5f4ae

Please sign in to comment.