From 38ad84f6e596f81c6abaad7c3b4b55253ab1bad3 Mon Sep 17 00:00:00 2001 From: Vildan Softic Date: Mon, 13 Jul 2026 17:24:23 +0200 Subject: [PATCH 1/2] feat(): onBeforeCellEdit hook for RowBasedEditing plugin provides the opportunity to define a callable hook to decide editor read/write on a per cell basis --- docs/grid-functionalities/row-based-edit.md | 2 + .../__tests__/slickRowBasedEdit.spec.ts | 43 +++++++++++++++---- .../src/extensions/slickRowBasedEdit.ts | 15 +++++-- .../rowBasedEditOption.interface.ts | 3 ++ 4 files changed, 51 insertions(+), 12 deletions(-) diff --git a/docs/grid-functionalities/row-based-edit.md b/docs/grid-functionalities/row-based-edit.md index 3369848f6..7efe2db79 100644 --- a/docs/grid-functionalities/row-based-edit.md +++ b/docs/grid-functionalities/row-based-edit.md @@ -33,6 +33,8 @@ If the [Excel Copy Buffer Plugin](excel-copy-buffer.md) is configured, the Row b The idea of the plugin is to focus the users editing experience on specific individual rows and and save them individually. This is achieved by letting the user toggle one or more rows into edit mode. When a that happens a potentially registered `onBeforeEditMode` callback is executed to handle various preparation or cleanup tasks. Now changes can be made to those rows and will be highlighted and tracked. The user may cancel the edit mode at any time and revert all cells changes. If the save button is pressed on the other hand an `onBeforeRowUpdated` hook, which you define via plugin options, is called and expects a `Promise`. In that method you'd typically write the changes to your backend and return either true or false based on the operations outcome. If a negative boolean is returned the edit mode is kept, otherwise the row applies the changes and toggles back into readonly mode. That means, no modifications can be done on the grid. Additionally, when a row is deleted via the delete button, an `onAfterDelete` callback is executed, which you can use to e.g. remove the row from your backend. +Additionally, you can also register the optional `onBeforeCellEdit` hook which allows to control on a per cell level whether editing should be allowed or not, by returning a boolean. + Here's the respective code shown in Example22: #### ViewModel diff --git a/packages/common/src/extensions/__tests__/slickRowBasedEdit.spec.ts b/packages/common/src/extensions/__tests__/slickRowBasedEdit.spec.ts index 4f0c83b1b..d3964bd7c 100644 --- a/packages/common/src/extensions/__tests__/slickRowBasedEdit.spec.ts +++ b/packages/common/src/extensions/__tests__/slickRowBasedEdit.spec.ts @@ -3,7 +3,7 @@ import { afterEach, beforeEach, describe, expect, it, vi, type Mock } from 'vite import { TranslateServiceStub } from '../../../../../test/translateServiceStub.js'; import { SlickEvent, type SlickGrid } from '../../core/index.js'; import { Editors } from '../../editors/editors.index.js'; -import type { Column, EditCommand, GridOption, OnBeforeEditCellEventArgs, OnSetOptionsEventArgs, RowBasedEditOptions } from '../../interfaces/index.js'; +import type { Column, EditCommand, GridOption, OnBeforeEditCellEventArgs, OnEventArgs, OnSetOptionsEventArgs, RowBasedEditOptions } from '../../interfaces/index.js'; import { GridService } from '../../services/grid.service.js'; import type { ExtensionUtility } from '../extensionUtility.js'; import { @@ -148,6 +148,33 @@ describe('Row Based Edit Plugin', () => { expect(onBeforeEditCellHandler({} as Event, { item: fakeItem } as OnBeforeEditCellEventArgs)).toBe(true); }); + it('should respect user provided custom onBeforeCellEdit hooks', () => { + const fakeItem = { id: 'test' }; + + gridStub.getData.mockReturnValue({ getItem: () => fakeItem }); + gridStub.getOptions.mockReturnValue({ ...optionsMock, }); + let userCanEdit = false; + plugin = new SlickRowBasedEdit(extensionUtilityStub, pubSubServiceStub, { ...addonOptions, onBeforeCellEdit: (_args: OnEventArgs) => (() => userCanEdit)() }); + (plugin as any)._eventHandler = { + subscribe: vi.fn(), + unsubscribeAll: vi.fn(), + }; + plugin.init(gridStub, gridService); + + const onBeforeEditCellHandler = (plugin.eventHandler.subscribe as Mock).mock.calls[0][1] as (e: Event, args: OnBeforeEditCellEventArgs) => boolean; + expect(onBeforeEditCellHandler({} as Event, { item: fakeItem } as OnBeforeEditCellEventArgs)).toBe(false); + + // set row to edit mode + plugin.rowBasedEditCommandHandler(fakeItem, {} as Column, {} as EditCommand); + + // should still fail + expect(onBeforeEditCellHandler({} as Event, { item: fakeItem } as OnBeforeEditCellEventArgs)).toBe(false); + + // change to allow in userland + userCanEdit = true; + expect(onBeforeEditCellHandler({} as Event, { item: fakeItem } as OnBeforeEditCellEventArgs)).toBe(true); + }); + it('should throw an error when "enableRowEdit" is set without "enableCellNavigation"', () => { gridStub.getOptions.mockReturnValue({}); @@ -212,7 +239,7 @@ describe('Row Based Edit Plugin', () => { { prevSerializedValue: 'foo', serializedValue: 'bar', - execute: () => {}, + execute: () => { }, } as EditCommand ); @@ -237,7 +264,7 @@ describe('Row Based Edit Plugin', () => { { prevSerializedValue: [], serializedValue: ['bar'], - execute: () => {}, + execute: () => { }, } as EditCommand ); @@ -413,7 +440,7 @@ describe('Row Based Edit Plugin', () => { { prevSerializedValue: 'foo', serializedValue: 'bar', - execute: () => {}, + execute: () => { }, } as EditCommand ); @@ -709,7 +736,7 @@ describe('Row Based Edit Plugin', () => { { prevSerializedValue: 'foo', serializedValue: 'bar', - execute: () => {}, + execute: () => { }, } as EditCommand ); gridStub.invalidate.mockClear(); @@ -775,7 +802,7 @@ describe('Row Based Edit Plugin', () => { { prevSerializedValue: 'foo', serializedValue: 'bar', - execute: () => {}, + execute: () => { }, } as EditCommand ); gridStub.invalidate.mockClear(); @@ -803,7 +830,7 @@ describe('Row Based Edit Plugin', () => { { prevSerializedValue: 'foo', serializedValue: 'bar', - execute: () => {}, + execute: () => { }, undo: undoSpy, } as unknown as EditCommand ); @@ -836,7 +863,7 @@ describe('Row Based Edit Plugin', () => { { prevSerializedValue: 'foo', serializedValue: 'bar', - execute: () => {}, + execute: () => { }, undo: undoSpy, } as unknown as EditCommand ); diff --git a/packages/common/src/extensions/slickRowBasedEdit.ts b/packages/common/src/extensions/slickRowBasedEdit.ts index a4af34d21..78b6bf7a9 100644 --- a/packages/common/src/extensions/slickRowBasedEdit.ts +++ b/packages/common/src/extensions/slickRowBasedEdit.ts @@ -59,7 +59,7 @@ export class SlickRowBasedEdit { private _existingEditCommandHandler: ((item: any, column: Column, command: EditCommand) => void) | undefined; protected _currentLang = 'en'; - private _translations: { [locale: string]: ButtonTranslation } = {}; + private _translations: { [locale: string]: ButtonTranslation; } = {}; /** Constructor of the SlickGrid 3rd party plugin, it can optionally receive options */ constructor( @@ -472,8 +472,15 @@ export class SlickRowBasedEdit { return actionFragment; } - protected onBeforeEditCellHandler = (_e: SlickEventData, args: OnBeforeEditCellEventArgs) => { - return this._editedRows.has(args.item?.[this.gridOptions.datasetIdPropertyName ?? 'id']) as boolean; + protected onBeforeEditCellHandler = (_e: SlickEventData, args: OnBeforeEditCellEventArgs): boolean => { + const hasOnBeforeCellEditFn = typeof this._addonOptions?.onBeforeCellEdit === 'function'; + const canEdit = this._editedRows.has(args.item?.[this.gridOptions.datasetIdPropertyName ?? 'id']) as boolean; + let userCanEdit = true; + if (hasOnBeforeCellEditFn) { + userCanEdit = this._addonOptions?.onBeforeCellEdit!(args as unknown as OnEventArgs) ?? true; + } + + return canEdit && userCanEdit; }; protected toggleEditmode(dataContext: any, editMode: boolean): void { @@ -491,7 +498,7 @@ export class SlickRowBasedEdit { this._grid.invalidate(); } - protected updateItemMetadata(previousItemMetadata: any): (rowNumber: number) => { cssClasses: string } { + protected updateItemMetadata(previousItemMetadata: any): (rowNumber: number) => { cssClasses: string; } { return (rowNumber: number) => { const item = this._grid.getData().getItem(rowNumber); let meta = { diff --git a/packages/common/src/interfaces/rowBasedEditOption.interface.ts b/packages/common/src/interfaces/rowBasedEditOption.interface.ts index 95841471b..aaac8dc36 100644 --- a/packages/common/src/interfaces/rowBasedEditOption.interface.ts +++ b/packages/common/src/interfaces/rowBasedEditOption.interface.ts @@ -76,6 +76,9 @@ export interface RowBasedEditOptions { /** method called before a row enters edit mode. */ onBeforeEditMode?: (args: OnEventArgs) => void; + /** method deciding whether an individual cell can be toggled to edit mode */ + onBeforeCellEdit?: (args: OnEventArgs) => boolean; + /** method called after a row is deleted. */ onAfterDelete?: (args: OnEventArgs) => void; } From 02c6cd63a68ed328610f7a4ce49ea2c271cb8b6f Mon Sep 17 00:00:00 2001 From: Vildan Softic Date: Mon, 13 Jul 2026 17:29:58 +0200 Subject: [PATCH 2/2] docs: add docs to framework specific mds --- .../grid-functionalities/row-based-edit.md | 2 ++ .../grid-functionalities/row-based-edit.md | 2 ++ .../grid-functionalities/row-based-edit.md | 2 ++ .../grid-functionalities/row-based-edit.md | 2 ++ .../__tests__/slickRowBasedEdit.spec.ts | 31 +++++++++++++------ .../src/extensions/slickRowBasedEdit.ts | 4 +-- 6 files changed, 31 insertions(+), 12 deletions(-) diff --git a/frameworks/angular-slickgrid/docs/grid-functionalities/row-based-edit.md b/frameworks/angular-slickgrid/docs/grid-functionalities/row-based-edit.md index 53f526153..c792fca25 100644 --- a/frameworks/angular-slickgrid/docs/grid-functionalities/row-based-edit.md +++ b/frameworks/angular-slickgrid/docs/grid-functionalities/row-based-edit.md @@ -33,6 +33,8 @@ If the [Excel Copy Buffer Plugin](excel-copy-buffer.md) is configured, the Row b The idea of the plugin is to focus the users editing experience on specific individual rows and and save them individually. This is achieved by letting the user toggle one or more rows into edit mode. When a that happens a potentially registered `onBeforeEditMode` callback is executed to handle various preparation or cleanup tasks. Now changes can be made to those rows and will be highlighted and tracked. The user may cancel the edit mode at any time and revert all cells changes. If the save button is pressed on the other hand an `onBeforeRowUpdated` hook, which you define via plugin options, is called and expects a `Promise`. In that method you'd typically write the changes to your backend and return either true or false based on the operations outcome. If a negative boolean is returned the edit mode is kept, otherwise the row applies the changes and toggles back into readonly mode. That means, no modifications can be done on the grid. Additionally, when a row is deleted via the delete button, an `onAfterDelete` callback is executed, which you can use to e.g. remove the row from your backend. +Additionally, you can also register the optional `onBeforeCellEdit` hook which allows to control on a per cell level whether editing should be allowed or not, by returning a boolean. + Here's the respective code shown in Example22: #### ViewModel diff --git a/frameworks/aurelia-slickgrid/docs/grid-functionalities/row-based-edit.md b/frameworks/aurelia-slickgrid/docs/grid-functionalities/row-based-edit.md index 44b29556a..4c748a750 100644 --- a/frameworks/aurelia-slickgrid/docs/grid-functionalities/row-based-edit.md +++ b/frameworks/aurelia-slickgrid/docs/grid-functionalities/row-based-edit.md @@ -33,6 +33,8 @@ If the [Excel Copy Buffer Plugin](excel-copy-buffer.md) is configured, the Row b The idea of the plugin is to focus the users editing experience on specific individual rows and and save them individually. This is achieved by letting the user toggle one or more rows into edit mode. When a that happens a potentially registered `onBeforeEditMode` callback is executed to handle various preparation or cleanup tasks. Now changes can be made to those rows and will be highlighted and tracked. The user may cancel the edit mode at any time and revert all cells changes. If the save button is pressed on the other hand an `onBeforeRowUpdated` hook, which you define via plugin options, is called and expects a `Promise`. In that method you'd typically write the changes to your backend and return either true or false based on the operations outcome. If a negative boolean is returned the edit mode is kept, otherwise the row applies the changes and toggles back into readonly mode. That means, no modifications can be done on the grid. Additionally, when a row is deleted via the delete button, an `onAfterDelete` callback is executed, which you can use to e.g. remove the row from your backend. +Additionally, you can also register the optional `onBeforeCellEdit` hook which allows to control on a per cell level whether editing should be allowed or not, by returning a boolean. + Here's the respective code shown in Example22: #### ViewModel diff --git a/frameworks/slickgrid-react/docs/grid-functionalities/row-based-edit.md b/frameworks/slickgrid-react/docs/grid-functionalities/row-based-edit.md index 6d14dcc02..a142cdaa9 100644 --- a/frameworks/slickgrid-react/docs/grid-functionalities/row-based-edit.md +++ b/frameworks/slickgrid-react/docs/grid-functionalities/row-based-edit.md @@ -33,6 +33,8 @@ If the [Excel Copy Buffer Plugin](excel-copy-buffer.md) is configured, the Row b The idea of the plugin is to focus the users editing experience on specific individual rows and and save them individually. This is achieved by letting the user toggle one or more rows into edit mode. When a that happens a potentially registered `onBeforeEditMode` callback is executed to handle various preparation or cleanup tasks. Now changes can be made to those rows and will be highlighted and tracked. The user may cancel the edit mode at any time and revert all cells changes. If the save button is pressed on the other hand an `onBeforeRowUpdated` hook, which you define via plugin options, is called and expects a `Promise`. In that method you'd typically write the changes to your backend and return either true or false based on the operations outcome. If a negative boolean is returned the edit mode is kept, otherwise the row applies the changes and toggles back into readonly mode. That means, no modifications can be done on the grid. Additionally, when a row is deleted via the delete button, an `onAfterDelete` callback is executed, which you can use to e.g. remove the row from your backend. +Additionally, you can also register the optional `onBeforeCellEdit` hook which allows to control on a per cell level whether editing should be allowed or not, by returning a boolean. + Here's the respective code shown in Example22: #### ViewModel diff --git a/frameworks/slickgrid-vue/docs/grid-functionalities/row-based-edit.md b/frameworks/slickgrid-vue/docs/grid-functionalities/row-based-edit.md index 21c09223f..b99f88d81 100644 --- a/frameworks/slickgrid-vue/docs/grid-functionalities/row-based-edit.md +++ b/frameworks/slickgrid-vue/docs/grid-functionalities/row-based-edit.md @@ -33,6 +33,8 @@ If the [Excel Copy Buffer Plugin](excel-copy-buffer.md) is configured, the Row b The idea of the plugin is to focus the users editing experience on specific individual rows and and save them individually. This is achieved by letting the user toggle one or more rows into edit mode. When a that happens a potentially registered `onBeforeEditMode` callback is executed to handle various preparation or cleanup tasks. Now changes can be made to those rows and will be highlighted and tracked. The user may cancel the edit mode at any time and revert all cells changes. If the save button is pressed on the other hand an `onBeforeRowUpdated` hook, which you define via plugin options, is called and expects a `Promise`. In that method you'd typically write the changes to your backend and return either true or false based on the operations outcome. If a negative boolean is returned the edit mode is kept, otherwise the row applies the changes and toggles back into readonly mode. That means, no modifications can be done on the grid. Additionally, when a row is deleted via the delete button, an `onAfterDelete` callback is executed, which you can use to e.g. remove the row from your backend. +Additionally, you can also register the optional `onBeforeCellEdit` hook which allows to control on a per cell level whether editing should be allowed or not, by returning a boolean. + Here's the respective code shown in Example22: #### ViewModel diff --git a/packages/common/src/extensions/__tests__/slickRowBasedEdit.spec.ts b/packages/common/src/extensions/__tests__/slickRowBasedEdit.spec.ts index d3964bd7c..fc12e8211 100644 --- a/packages/common/src/extensions/__tests__/slickRowBasedEdit.spec.ts +++ b/packages/common/src/extensions/__tests__/slickRowBasedEdit.spec.ts @@ -3,7 +3,15 @@ import { afterEach, beforeEach, describe, expect, it, vi, type Mock } from 'vite import { TranslateServiceStub } from '../../../../../test/translateServiceStub.js'; import { SlickEvent, type SlickGrid } from '../../core/index.js'; import { Editors } from '../../editors/editors.index.js'; -import type { Column, EditCommand, GridOption, OnBeforeEditCellEventArgs, OnEventArgs, OnSetOptionsEventArgs, RowBasedEditOptions } from '../../interfaces/index.js'; +import type { + Column, + EditCommand, + GridOption, + OnBeforeEditCellEventArgs, + OnEventArgs, + OnSetOptionsEventArgs, + RowBasedEditOptions, +} from '../../interfaces/index.js'; import { GridService } from '../../services/grid.service.js'; import type { ExtensionUtility } from '../extensionUtility.js'; import { @@ -152,9 +160,12 @@ describe('Row Based Edit Plugin', () => { const fakeItem = { id: 'test' }; gridStub.getData.mockReturnValue({ getItem: () => fakeItem }); - gridStub.getOptions.mockReturnValue({ ...optionsMock, }); + gridStub.getOptions.mockReturnValue({ ...optionsMock }); let userCanEdit = false; - plugin = new SlickRowBasedEdit(extensionUtilityStub, pubSubServiceStub, { ...addonOptions, onBeforeCellEdit: (_args: OnEventArgs) => (() => userCanEdit)() }); + plugin = new SlickRowBasedEdit(extensionUtilityStub, pubSubServiceStub, { + ...addonOptions, + onBeforeCellEdit: (_args: OnEventArgs) => (() => userCanEdit)(), + }); (plugin as any)._eventHandler = { subscribe: vi.fn(), unsubscribeAll: vi.fn(), @@ -239,7 +250,7 @@ describe('Row Based Edit Plugin', () => { { prevSerializedValue: 'foo', serializedValue: 'bar', - execute: () => { }, + execute: () => {}, } as EditCommand ); @@ -264,7 +275,7 @@ describe('Row Based Edit Plugin', () => { { prevSerializedValue: [], serializedValue: ['bar'], - execute: () => { }, + execute: () => {}, } as EditCommand ); @@ -440,7 +451,7 @@ describe('Row Based Edit Plugin', () => { { prevSerializedValue: 'foo', serializedValue: 'bar', - execute: () => { }, + execute: () => {}, } as EditCommand ); @@ -736,7 +747,7 @@ describe('Row Based Edit Plugin', () => { { prevSerializedValue: 'foo', serializedValue: 'bar', - execute: () => { }, + execute: () => {}, } as EditCommand ); gridStub.invalidate.mockClear(); @@ -802,7 +813,7 @@ describe('Row Based Edit Plugin', () => { { prevSerializedValue: 'foo', serializedValue: 'bar', - execute: () => { }, + execute: () => {}, } as EditCommand ); gridStub.invalidate.mockClear(); @@ -830,7 +841,7 @@ describe('Row Based Edit Plugin', () => { { prevSerializedValue: 'foo', serializedValue: 'bar', - execute: () => { }, + execute: () => {}, undo: undoSpy, } as unknown as EditCommand ); @@ -863,7 +874,7 @@ describe('Row Based Edit Plugin', () => { { prevSerializedValue: 'foo', serializedValue: 'bar', - execute: () => { }, + execute: () => {}, undo: undoSpy, } as unknown as EditCommand ); diff --git a/packages/common/src/extensions/slickRowBasedEdit.ts b/packages/common/src/extensions/slickRowBasedEdit.ts index 78b6bf7a9..a8a5abcf4 100644 --- a/packages/common/src/extensions/slickRowBasedEdit.ts +++ b/packages/common/src/extensions/slickRowBasedEdit.ts @@ -59,7 +59,7 @@ export class SlickRowBasedEdit { private _existingEditCommandHandler: ((item: any, column: Column, command: EditCommand) => void) | undefined; protected _currentLang = 'en'; - private _translations: { [locale: string]: ButtonTranslation; } = {}; + private _translations: { [locale: string]: ButtonTranslation } = {}; /** Constructor of the SlickGrid 3rd party plugin, it can optionally receive options */ constructor( @@ -498,7 +498,7 @@ export class SlickRowBasedEdit { this._grid.invalidate(); } - protected updateItemMetadata(previousItemMetadata: any): (rowNumber: number) => { cssClasses: string; } { + protected updateItemMetadata(previousItemMetadata: any): (rowNumber: number) => { cssClasses: string } { return (rowNumber: number) => { const item = this._grid.getData().getItem(rowNumber); let meta = {