Skip to content

Commit

Permalink
feat: add preRegisterExternalExtensions to help external RowDetail
Browse files Browse the repository at this point in the history
- provide a new `preRegisterExternalExtensions()` callback in the grid options to help multiple extensions to work together.
- add Row Selection + Row Detail to Example 21
- that will mostly help only Slickgrid-Universal (and won't help Angular/Aurelia projects but it might be useful in the future, who knows)
  • Loading branch information
ghiscoding committed Jul 27, 2024
1 parent 64c4c8b commit 016af41
Show file tree
Hide file tree
Showing 7 changed files with 102 additions and 42 deletions.
33 changes: 21 additions & 12 deletions examples/vite-demo-vanilla-bundle/src/examples/example21.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { BindingEventService } from '@slickgrid-universal/binding';
import { type Column, createDomElement, FieldType, Filters, Formatters, type GridOption, SlickEventHandler, Editors } from '@slickgrid-universal/common';
import { type Column, createDomElement, FieldType, Filters, Formatters, type GridOption, SlickEventHandler, Editors, ExtensionName } from '@slickgrid-universal/common';
import { SlickRowDetailView } from '@slickgrid-universal/row-detail-view-plugin';
import { Slicker, type SlickVanillaGridBundle } from '@slickgrid-universal/vanilla-bundle';

Expand Down Expand Up @@ -52,12 +52,6 @@ export default class Example21 {
this.gridContainerElm = document.querySelector<HTMLDivElement>(`.grid21`) as HTMLDivElement;

this.sgb = new Slicker.GridBundle(this.gridContainerElm, this.columnDefinitions, { ...ExampleGridOptions, ...this.gridOptions }, this.dataset);
this.rowDetail = new SlickRowDetailView(this.sgb.instances!.eventPubSubService!);
this.rowDetail.create(this.columnDefinitions, this.gridOptions);
this.rowDetail.init(this.sgb.slickGrid!);

// we need to sync the column definitions to include the new row detail icon column in the grid
this.sgb.columnDefinitions = this.columnDefinitions.slice();

// add all row detail event listeners
this.addRowDetailEventHandlers();
Expand Down Expand Up @@ -86,19 +80,22 @@ export default class Example21 {
autoResize: {
container: '.demo-container',
},
enableColumnReorder: true,
enableFiltering: true,
enableRowDetailView: true,
rowSelectionOptions: {
selectActiveRow: true
preRegisterExternalExtensions: (pubSubService) => {
// Row Detail View is a special case because of its requirement to create extra column definition dynamically
// so it must be pre-registered before SlickGrid is instantiated, we can do so via this option
this.rowDetail = new SlickRowDetailView(pubSubService);
return [{ name: ExtensionName.rowDetailView, instance: this.rowDetail }];
},
rowHeight: 33,
rowDetailView: {
columnIndexPosition: 1,
cssClass: 'detail-view-toggle',
preTemplate: this.loadingTemplate.bind(this),
postTemplate: this.loadView.bind(this),
process: this.simulateServerAsyncCall.bind(this),
useRowClick: true,
useRowClick: false,

// how many grid rows do we want to use for the detail panel
// also note that the detail view adds an extra 1 row for padding purposes
Expand All @@ -109,7 +106,19 @@ export default class Example21 {
// by using the override function to provide custom logic of which row is expandable
// you can override it here in the options or externally by calling the method on the plugin instance
expandableOverride: (_row, dataContext) => dataContext.id % 2 === 1,
}
},
rowSelectionOptions: {
// True (Single Selection), False (Multiple Selections)
selectActiveRow: false
},

// You could also enable Row Selection as well, but just make sure to disable `useRowClick: false`
enableCheckboxSelector: true,
enableRowSelection: true,
checkboxSelector: {
hideInFilterHeaderRow: false,
hideSelectAllCheckbox: true,
},
};
}

Expand Down
2 changes: 2 additions & 0 deletions packages/common/src/interfaces/extensionModel.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,6 @@ export interface ExtensionModel<P extends (SlickControlList | SlickPluginList)>

/** Instance of the Addon (3rd party SlickGrid Control or Plugin) */
instance: P;

columnIndexPosition?: number;
}
9 changes: 8 additions & 1 deletion packages/common/src/interfaces/gridOption.interface.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { EventNamingStyle } from '@slickgrid-universal/event-pub-sub';
import type { BasePubSubService, EventNamingStyle } from '@slickgrid-universal/event-pub-sub';
import type { MultipleSelectOption } from 'multiple-select-vanilla';

import type {
Expand All @@ -20,6 +20,7 @@ import type {
EmptyWarning,
ExcelCopyBufferOption,
ExcelExportOption,
ExtensionModel,
ExternalResource,
Formatter,
FormatterOption,
Expand Down Expand Up @@ -526,6 +527,12 @@ export interface GridOption<C extends Column = Column> {
/** Register any external Resources (Components, Services) like the ExcelExportService, TextExportService, SlickCompositeEditorComponent, ... */
externalResources?: ExternalResource[];

/**
* Some external (optional) extensions might need to be pre-registered, for example SlickRowDetail.
* Note: this will not only call `create()` method before SlickGrid instantiation, but will also call its `init()` method
*/
preRegisterExternalExtensions?: (eventPubSubService: BasePubSubService) => Array<ExtensionModel<any>>;

/**
* Default to 0, how long to wait between each characters that the user types before processing the filtering process (only applies for local/in-memory grid).
* Especially useful when you have a big dataset and you want to limit the amount of search called (by default every keystroke will trigger a search on the dataset and that is sometime slow).
Expand Down
32 changes: 32 additions & 0 deletions packages/common/src/services/__tests__/extension.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -647,6 +647,38 @@ describe('ExtensionService', () => {
expect(extCheckSelectSpy).toHaveBeenCalledWith(columnsMock, gridOptionsMock);
expect(extRowMoveSpy).toHaveBeenCalledWith(columnsMock, gridOptionsMock);
});

it('should create & init external extensions when "preRegisterExternalExtensions" is set in the grid options', () => {
const createMock = jest.fn();
const initMock = jest.fn();
class ExternalExtension {
create(columnDefinitions: Column[], gridOptions: GridOption) {
createMock(columnDefinitions, gridOptions);
}
init(grid: SlickGrid) {
initMock(grid);
}
}

const columnsMock = [{ id: 'field1', field: 'field1', width: 100, cssClass: 'red' }, { id: 'field2', field: 'field2', width: 50, }] as Column[];
const gridOptionsMock = {
enableCheckboxSelector: true, enableRowSelection: true,
checkboxSelector: { columnIndexPosition: 1 },
preRegisterExternalExtensions: (pubSub) => {
const ext = new ExternalExtension();
return [{ name: ExtensionName.rowDetailView, instance: ext }];
}
} as GridOption;
const extCheckSelectSpy = jest.spyOn(mockCheckboxSelectColumn, 'create');

service.createExtensionsBeforeGridCreation(columnsMock, gridOptionsMock);

expect(extCheckSelectSpy).toHaveBeenCalledWith(columnsMock, gridOptionsMock);
expect(createMock).toHaveBeenCalledWith(columnsMock, gridOptionsMock);

service.bindDifferentExtensions();
expect(initMock).toHaveBeenCalledWith(gridStub);
});
});

it('should call hideColumn and expect "visibleColumns" to be updated accordingly', () => {
Expand Down
38 changes: 24 additions & 14 deletions packages/common/src/services/extension.service.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { type BasePubSubService } from '@slickgrid-universal/event-pub-sub';

import type { Column, ExtensionModel, GridOption, SlickRowDetailView, } from '../interfaces/index';
import type { Column, ExtensionModel, GridOption } from '../interfaces/index';
import { type ColumnReorderFunction, type ExtensionList, ExtensionName, type InferExtensionByName, type SlickControlList, type SlickPluginList } from '../enums/index';
import type { SharedService } from './shared.service';
import type { TranslaterService } from './translater.service';
Expand All @@ -26,12 +26,6 @@ import type { SortService } from './sort.service';
import type { TreeDataService } from './treeData.service';
import type { GridService } from './grid.service';

interface ExtensionWithColumnIndexPosition {
name: ExtensionName;
columnIndexPosition: number;
extension: SlickCheckboxSelectColumn | SlickRowDetailView | SlickRowMoveManager | SlickRowBasedEdit;
}

export class ExtensionService {
protected _extensionCreatedList: ExtensionList<any> = {} as ExtensionList<any>;
protected _extensionList: ExtensionList<any> = {} as ExtensionList<any>;
Expand All @@ -48,6 +42,7 @@ export class ExtensionService {
protected _rowMoveManagerPlugin?: SlickRowMoveManager;
protected _rowSelectionModel?: SlickRowSelectionModel;
protected _rowBasedEdit?: SlickRowBasedEdit;
protected _requireInitExternalExtensions: Array<ExtensionModel<any>> = [];

get extensionList(): ExtensionList<any> {
return this._extensionList;
Expand Down Expand Up @@ -302,6 +297,12 @@ export class ExtensionService {
this._extensionList[ExtensionName.rowMoveManager] = { name: ExtensionName.rowMoveManager, instance: this._rowMoveManagerPlugin };
}
}

if (this._requireInitExternalExtensions.length) {
this._requireInitExternalExtensions.forEach(extension => {
extension.instance.init(this.sharedService.slickGrid, undefined as any);
});
}
}
}

Expand All @@ -312,29 +313,38 @@ export class ExtensionService {
* @param gridOptions
*/
createExtensionsBeforeGridCreation(columnDefinitions: Column[], gridOptions: GridOption): void {
const featureWithColumnIndexPositions: ExtensionWithColumnIndexPosition[] = [];
const featureWithColumnIndexPositions: Array<ExtensionModel<any>> = [];

// the following 3 features might have `columnIndexPosition` that we need to respect their column order, we will execute them by their sort order further down
// we push them into a array and we'll process them by their position (if provided, else use same order that they were inserted)
if (gridOptions.enableCheckboxSelector) {
if (!this.getCreatedExtensionByName(ExtensionName.checkboxSelector)) {
this._checkboxSelectColumn = new SlickCheckboxSelectColumn(this.pubSubService, this.sharedService.gridOptions.checkboxSelector);
featureWithColumnIndexPositions.push({ name: ExtensionName.checkboxSelector, extension: this._checkboxSelectColumn, columnIndexPosition: gridOptions?.checkboxSelector?.columnIndexPosition ?? featureWithColumnIndexPositions.length });
featureWithColumnIndexPositions.push({ name: ExtensionName.checkboxSelector, instance: this._checkboxSelectColumn, columnIndexPosition: gridOptions?.checkboxSelector?.columnIndexPosition ?? featureWithColumnIndexPositions.length });
}
}
if (gridOptions.enableRowMoveManager) {
if (!this.getCreatedExtensionByName(ExtensionName.rowMoveManager)) {
this._rowMoveManagerPlugin = new SlickRowMoveManager(this.pubSubService);
featureWithColumnIndexPositions.push({ name: ExtensionName.rowMoveManager, extension: this._rowMoveManagerPlugin, columnIndexPosition: gridOptions?.rowMoveManager?.columnIndexPosition ?? featureWithColumnIndexPositions.length });
featureWithColumnIndexPositions.push({ name: ExtensionName.rowMoveManager, instance: this._rowMoveManagerPlugin, columnIndexPosition: gridOptions?.rowMoveManager?.columnIndexPosition ?? featureWithColumnIndexPositions.length });
}
}
if (gridOptions.enableRowBasedEdit) {
if (!this.getCreatedExtensionByName(ExtensionName.rowBasedEdit)) {
this._rowBasedEdit = new SlickRowBasedEdit(this.extensionUtility, this.pubSubService);
featureWithColumnIndexPositions.push({ name: ExtensionName.rowBasedEdit, extension: this._rowBasedEdit, columnIndexPosition: gridOptions?.rowMoveManager?.columnIndexPosition ?? featureWithColumnIndexPositions.length });
featureWithColumnIndexPositions.push({ name: ExtensionName.rowBasedEdit, instance: this._rowBasedEdit, columnIndexPosition: gridOptions?.rowMoveManager?.columnIndexPosition ?? featureWithColumnIndexPositions.length });
}
}

// user could also optionally preRegister some external resources (extensions)
if (gridOptions.preRegisterExternalExtensions) {
const extraExtensions = gridOptions.preRegisterExternalExtensions(this.pubSubService);
extraExtensions.forEach(extension => {
featureWithColumnIndexPositions.push(extension);
this._requireInitExternalExtensions.push(extension);
});
}

// since some features could have a `columnIndexPosition`, we need to make sure these indexes are respected in the column definitions
this.createExtensionByTheirColumnIndex(featureWithColumnIndexPositions, columnDefinitions, gridOptions);

Expand Down Expand Up @@ -494,13 +504,13 @@ export class ExtensionService {
* @param columnDefinitions
* @param gridOptions
*/
protected createExtensionByTheirColumnIndex(featureWithIndexPositions: ExtensionWithColumnIndexPosition[], columnDefinitions: Column[], gridOptions: GridOption): void {
protected createExtensionByTheirColumnIndex(featureWithIndexPositions: Array<ExtensionModel<any>>, columnDefinitions: Column[], gridOptions: GridOption): void {
// 1- first step is to sort them by their index position
featureWithIndexPositions.sort((feat1, feat2) => feat1.columnIndexPosition - feat2.columnIndexPosition);
featureWithIndexPositions.sort((feat1, feat2) => (feat1?.columnIndexPosition ?? 0) - (feat2?.columnIndexPosition ?? 0));

// 2- second step, we can now proceed to create each extension/addon and that will position them accordingly in the column definitions list
featureWithIndexPositions.forEach(feature => {
const instance = feature.extension.create(columnDefinitions, gridOptions);
const instance = feature.instance.create(columnDefinitions, gridOptions);
if (instance) {
this._extensionCreatedList[feature.name] = { name: feature.name, instance };
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ import {
unsubscribeAll,
SlickEventHandler,
SlickDataView,
SlickGrid
SlickGrid,
} from '@slickgrid-universal/common';
import { extend } from '@slickgrid-universal/utils';
import { EventNamingStyle, EventPubSubService } from '@slickgrid-universal/event-pub-sub';
Expand Down
28 changes: 14 additions & 14 deletions test/cypress/e2e/example21.cy.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
describe('Example 21 - Row Detail View', () => {
const fullTitles = ['', 'Title', 'Duration', '% Complete', 'Start', 'Finish', 'Effort Driven'];
const fullTitles = ['', '', 'Title', 'Duration', '% Complete', 'Start', 'Finish', 'Effort Driven'];
const GRID_ROW_HEIGHT = 33;

it('should display Example title', () => {
Expand Down Expand Up @@ -101,7 +101,7 @@ describe('Example 21 - Row Detail View', () => {
if (index > expectedTasks.length - 1) {
return;
}
cy.wrap($row).children('.slick-cell:nth(1)')
cy.wrap($row).children('.slick-cell:nth(2)')
.first()
.should('contain', expectedTasks[index]);
});
Expand Down Expand Up @@ -157,7 +157,7 @@ describe('Example 21 - Row Detail View', () => {
if (index > expectedTasks.length - 1) {
return;
}
cy.wrap($row).children('.slick-cell:nth(1)')
cy.wrap($row).children('.slick-cell:nth(2)')
.first()
.should('contain', expectedTasks[index]);
});
Expand All @@ -179,7 +179,7 @@ describe('Example 21 - Row Detail View', () => {
.contains('Task 1');

cy.get('.grid21')
.find('.slick-row:nth(9)')
.find('.slick-row:nth(9) .slick-cell:nth(1)')
.click();

cy.get('.grid21')
Expand All @@ -191,7 +191,7 @@ describe('Example 21 - Row Detail View', () => {
.contains('Task 5');

cy.get('.grid21')
.find('.slick-header-column:nth(1)')
.find('.slick-header-column:nth(2)')
.trigger('mouseover')
.children('.slick-header-menu-button')
.should('be.hidden')
Expand All @@ -206,7 +206,7 @@ describe('Example 21 - Row Detail View', () => {
.click();

cy.get('.grid21')
.find('.slick-header-column:nth(1)')
.find('.slick-header-column:nth(2)')
.trigger('mouseover')
.children('.slick-header-menu-button')
.invoke('show')
Expand All @@ -220,7 +220,7 @@ describe('Example 21 - Row Detail View', () => {
.click();

cy.get('.grid21')
.find('.slick-header-column:nth(1)')
.find('.slick-header-column:nth(2)')
.find('.slick-sort-indicator-asc')
.should('have.length', 1);

Expand All @@ -241,19 +241,19 @@ describe('Example 21 - Row Detail View', () => {
if (index > expectedTasks.length - 1) {
return;
}
cy.wrap($row).children('.slick-cell:nth(1)')
cy.wrap($row).children('.slick-cell:nth(2)')
.first()
.should('contain', expectedTasks[index]);
});
});

it('should click open Row Detail of Task 1 and Task 101 then type a title filter of "Task 101" and expect Row Detail to be opened and still be rendered', () => {
cy.get('.grid21')
.find('.slick-row:nth(4)')
.find('.slick-row:nth(4) .slick-cell:nth(1)')
.click();

cy.get('.grid21')
.find('.slick-row:nth(1)')
.find('.slick-row:nth(1) .slick-cell:nth(1)')
.click();

cy.get('.grid21')
Expand Down Expand Up @@ -316,7 +316,7 @@ describe('Example 21 - Row Detail View', () => {

it('should click on 5th row detail open icon and expect it to open', () => {
cy.get('.grid21')
.find('.slick-row:nth(4) .slick-cell:nth(0)')
.find('.slick-row:nth(4) .slick-cell:nth(1)')
.click();

cy.get('.grid21')
Expand All @@ -330,8 +330,8 @@ describe('Example 21 - Row Detail View', () => {

it('should click on 2nd row "Title" cell to edit it and expect Task 5 row detail to get closed', () => {
cy.get('.grid21')
.find('.slick-row:nth(1) .slick-cell:nth(1)')
.click();
.find('.slick-row:nth(1) .slick-cell:nth(2)')
.dblclick();

cy.get('.editor-title')
.invoke('val')
Expand All @@ -348,7 +348,7 @@ describe('Example 21 - Row Detail View', () => {
cy.get('.slick-viewport-top.slick-viewport-left')
.scrollTo('top');

cy.get(`.slick-row[style="top: ${GRID_ROW_HEIGHT * 8}px;"] .slick-cell:nth(1)`)
cy.get(`.slick-row[style="top: ${GRID_ROW_HEIGHT * 8}px;"] .slick-cell:nth(2)`)
.click()
.wait(40);

Expand Down

0 comments on commit 016af41

Please sign in to comment.