Events & Commands
GridStorm uses two complementary systems for communication: the EventBus for notifications (something happened) and the CommandBus for mutations (make something happen). Events flow outward to inform you of state changes. Commands flow inward to trigger state changes. Together they enforce a strict unidirectional data flow.
Events vs Commands
| Aspect |
Events |
Commands |
| Direction |
Outward (grid to your code) |
Inward (your code to grid) |
| Purpose |
Notify that something happened |
Request that something changes |
| Timing |
Fired after state has changed |
Processed immediately when dispatched |
| Handlers |
Multiple listeners, no return value |
Multiple handlers, all invoked |
| Cancellation |
Not cancellable |
Cancellable via middleware |
Installation
Both systems are part of @gridstorm/core and are created automatically with the grid engine:
pnpm add @gridstorm/core
EventBus API
on(event, listener)
Subscribe to an event. Returns an unsubscribe function:
const unsub = engine.eventBus.on('selection:changed', (event) => {
console.log('Selected:', event.selectedNodes.length);
console.log('Source:', event.source);
});
// Later: unsubscribe
unsub();
once(event, listener)
Subscribe to an event for a single firing. The listener is automatically removed after it runs:
engine.eventBus.once('grid:ready', (event) => {
console.log('Grid is ready, API:', event.api);
});
off(event, listener)
Manually remove a specific listener by reference:
function onSort(event) {
console.log('Sort changed:', event.sortModel);
}
engine.eventBus.on('column:sort:changed', onSort);
// Later: remove by reference
engine.eventBus.off('column:sort:changed', onSort);
emit(event, payload)
Emit an event to all registered listeners. Primarily used by plugins and the engine internals:
engine.eventBus.emit('row:clicked', {
node: rowNode,
event: mouseEvent,
});
Listeners that throw errors are caught and logged without interrupting other listeners.
Via the GridApi
The GridApi wraps the EventBus with addEventListener and removeEventListener:
const unsub = api.addEventListener('filter:changed', (event) => {
console.log('Filters:', event.filterModel);
});
// The return value is an unsubscribe function
unsub();
Via React Hooks
In React, use the useGridEvent hook which handles the subscription lifecycle automatically:
import { useGridEvent } from '@gridstorm/react';
useGridEvent('cell:clicked', (event) => {
console.log('Clicked:', event.colId, event.value);
});
Or use event callback props on the <GridStorm> component:
<GridStorm
columns={columns}
rowData={data}
onSortChanged={(e) => console.log('Sort:', e.sortModel)}
onSelectionChanged={(e) => console.log('Selected:', e.selectedNodes.length)}
onCellValueChanged={(e) => console.log('Changed:', e.colId, e.newValue)}
/>
CommandBus API
registerHandler(commandType, handler)
Register a synchronous handler for a command type. Multiple handlers per type are supported -- all are invoked when the command is dispatched. Returns an unsubscribe function:
const unregister = engine.commandBus.registerHandler('sort:set', (payload) => {
console.log('Sort model set to:', payload.sortModel);
});
// Later: remove the handler
unregister();
registerAsyncHandler(commandType, handler)
Register an asynchronous handler. Async handlers are only invoked via dispatchAsync() and run sequentially (each awaited before the next):
engine.commandBus.registerAsyncHandler('ssrm:refresh', async (payload) => {
const data = await fetch('/api/rows');
engine.store.setState((prev) => ({ ...prev, /* ... */ }));
});
dispatch(commandType, payload)
Dispatch a command synchronously. Middleware runs first, then all registered handlers:
engine.commandBus.dispatch('sort:set', {
sortModel: [{ colId: 'name', sort: 'asc' }],
});
If no handler is registered and __GRIDSTORM_DEV__ is enabled, a warning is logged.
dispatchAsync(commandType, payload)
Dispatch a command asynchronously. Middleware runs synchronously first. Then sync handlers run, followed by async handlers in sequence:
await engine.commandBus.dispatchAsync('ssrm:refresh', {});
Command Middleware
Middleware intercepts every command before handlers run. Use it for logging, validation, analytics, or cancellation:
const removeMiddleware = engine.commandBus.use((context) => {
console.log(`[Command] ${context.commandType}`, context.payload);
});
// Later: remove
removeMiddleware();
Cancelling Commands
Call context.cancel() to prevent a command from reaching its handlers:
engine.commandBus.use((context) => {
if (context.commandType === 'editing:start') {
const { colId } = context.payload;
if (colId === 'id') {
console.warn('Cannot edit the ID column');
context.cancel();
}
}
});
Middleware Context
The CommandContext object passed to middleware contains:
| Property |
Type |
Description |
commandType |
string |
The command being dispatched |
payload |
any |
The command payload |
cancel() |
() => void |
Call to prevent handler execution |
Middleware functions run in registration order. If any middleware calls cancel(), no subsequent middleware or handlers run.
Complete Event Reference
Lifecycle Events
| Event |
Payload |
Description |
grid:ready |
{ api } |
Grid engine is initialized and ready |
grid:destroyed |
{} |
Grid has been destroyed |
Data Events
| Event |
Payload |
Description |
rowData:changed |
{ rowData } |
Row data was replaced via setRowData() |
rowNode:updated |
{ node } |
A specific row node was updated |
Column Events
| Event |
Payload |
Description |
columns:changed |
{ columns } |
Column definitions or state changed |
column:moved |
{ column, fromIndex, toIndex } |
Column was reordered |
column:resized |
{ column, oldWidth, newWidth, finished } |
Column width changed |
column:visible |
{ column, visible } |
Column visibility toggled |
column:pinned |
{ column, pinned } |
Column pin state changed |
column:sort:changed |
{ sortModel } |
Sort model changed |
Selection Events
| Event |
Payload |
Description |
selection:changed |
{ selectedNodes, source } |
Row selection changed |
range:selection:changed |
{ ranges } |
Cell range selection changed |
The source field indicates what triggered the selection: 'api', 'click', 'checkbox', 'keyboard', or 'selectAll'.
Editing Events
| Event |
Payload |
Description |
cell:editingStarted |
{ node, colId, value } |
Cell editing began |
cell:editingStopped |
{ node, colId, oldValue, newValue, cancelled } |
Cell editing ended |
cell:valueChanged |
{ node, colId, oldValue, newValue } |
Cell value was committed |
Filter Events
| Event |
Payload |
Description |
filter:changed |
{ filterModel } |
Column filter model changed |
quickFilter:changed |
{ text } |
Quick filter text changed |
Scroll and Viewport Events
| Event |
Payload |
Description |
scroll:changed |
{ top, left } |
Scroll position changed |
viewport:changed |
{ firstRow, lastRow } |
Visible row range changed |
Row Interaction Events
| Event |
Payload |
Description |
row:clicked |
{ node, event } |
Row was clicked |
row:doubleClicked |
{ node, event } |
Row was double-clicked |
cell:clicked |
{ node, colId, value, event } |
Cell was clicked |
cell:doubleClicked |
{ node, colId, value, event } |
Cell was double-clicked |
cell:focused |
{ position, previousPosition } |
Focused cell changed |
Row Grouping Events
| Event |
Payload |
Description |
row:groupOpened |
{ node, expanded } |
Group row expanded or collapsed |
grouping:changed |
{ groupColumns } |
Active group columns changed |
Aggregation Events
| Event |
Payload |
Description |
aggregation:computed |
{ groupNodeIds } |
Aggregation values recomputed |
Pagination Events
| Event |
Payload |
Description |
pagination:changed |
{ currentPage, totalPages, pageSize } |
Pagination state changed |
Pivot Events
| Event |
Payload |
Description |
pivot:changed |
{ pivotColumns, pivotMode } |
Pivot configuration changed |
Context Menu Events
| Event |
Payload |
Description |
contextMenu:opened |
{ node, colId, x, y } |
Context menu was opened |
contextMenu:closed |
{} |
Context menu was closed |
DOM Renderer Events
| Event |
Payload |
Description |
dom:headerRendered |
{} |
Header DOM was rebuilt (plugins re-inject handles) |
Row Reorder Events
| Event |
Payload |
Description |
row:moved |
{ rowId, fromIndex, toIndex } |
Row was moved to a new position |
row:dragStarted |
{ rowId } |
Row drag reorder started |
row:dragEnded |
{ rowId } |
Row drag reorder ended |
Clipboard Events
| Event |
Payload |
Description |
clipboard:copy |
{ data } |
Data was copied to clipboard |
clipboard:paste |
{ data } |
Data was pasted from clipboard |
clipboard:cut |
{ data } |
Data was cut to clipboard |
Complete Command Reference
Core Engine Commands
| Command |
Payload |
Description |
rows:reprocess |
{} |
Re-run filter, sort, and display pipeline |
sort:set |
{ sortModel } |
Set the sort model |
filter:set |
{ filterModel } |
Set the filter model |
Sorting Plugin Commands
| Command |
Payload |
Description |
sort:toggle |
{ colId, multiSort? } |
Toggle sort on a column |
sort:clear |
{} |
Clear all sort |
Filtering Plugin Commands
| Command |
Payload |
Description |
filter:set |
{ colId, model } |
Set filter on a specific column |
filter:clear |
{} |
Clear all filters |
filter:quickFilter |
{ text } |
Set quick filter text |
filter:setColumn |
{ colId, model } |
Set filter for one column |
filter:removeColumn |
{ colId } |
Remove filter from one column |
Selection Plugin Commands
| Command |
Payload |
Description |
selection:select |
{ rowId, multiSelect?, rangeSelect?, source? } |
Select a row |
selection:selectAll |
{} |
Select all visible rows |
selection:deselectAll |
{} |
Deselect all rows |
focus:set |
{ position } |
Set the focused cell |
Editing Plugin Commands
| Command |
Payload |
Description |
editing:start |
{ rowId, colId } |
Start editing a cell |
editing:stop |
{ cancel? } |
Stop editing (commit or cancel) |
editing:setValue |
{ value } |
Update the value during editing |
Pagination Plugin Commands
| Command |
Payload |
Description |
pagination:goToPage |
{ page } |
Navigate to a specific page |
pagination:nextPage |
{} |
Go to next page |
pagination:prevPage |
{} |
Go to previous page |
pagination:firstPage |
{} |
Go to first page |
pagination:lastPage |
{} |
Go to last page |
pagination:setPageSize |
{ pageSize } |
Change the page size |
Column Pinning Commands
| Command |
Payload |
Description |
column:pin |
{ colId, pinned } |
Pin or unpin a column |
column:unpinAll |
{} |
Unpin all columns |
Column Resize Commands
| Command |
Payload |
Description |
column:resize |
{ colId, delta } |
Resize by a pixel delta |
column:resizeStart |
{ colId, startX } |
Begin drag resize |
column:autoSize |
{ colId } |
Auto-size to content |
column:autoSizeAll |
{} |
Auto-size all columns |
Column Reorder Commands
| Command |
Payload |
Description |
column:move |
{ colId, toIndex } |
Move column to index |
column:swap |
{ colIdA, colIdB } |
Swap two columns |
column:dragStart |
{ colId, startX } |
Begin drag reorder |
Grouping Plugin Commands
| Command |
Payload |
Description |
group:addColumn |
{ colId } |
Add column to grouping |
group:removeColumn |
{ colId } |
Remove column from grouping |
group:setColumns |
{ colIds } |
Set all group columns |
group:expand |
{ groupId } |
Expand a group row |
group:collapse |
{ groupId } |
Collapse a group row |
group:expandAll |
{} |
Expand all groups |
group:collapseAll |
{} |
Collapse all groups |
group:expandToLevel |
{ level } |
Expand groups to a depth |
Aggregation Plugin Commands
| Command |
Payload |
Description |
agg:setColumnFunc |
{ colId, aggFunc } |
Set aggregation function on a column |
agg:removeColumnFunc |
{ colId } |
Remove aggregation from a column |
agg:compute |
{} |
Manually trigger aggregation |
Context Menu Commands
| Command |
Payload |
Description |
contextMenu:show |
{ x, y, node, colId, value } |
Show the context menu |
contextMenu:hide |
{} |
Hide the context menu |
contextMenu:registerItem |
{ item } |
Register a custom menu item |
Clipboard Commands
| Command |
Payload |
Description |
clipboard:copy |
{} |
Copy selected rows to clipboard |
clipboard:cut |
{} |
Cut selected rows to clipboard |
clipboard:paste |
{} |
Paste from clipboard |
clipboard:copyRange |
{ startRow, endRow, startCol, endCol } |
Copy a specific range |
Row Reorder Commands
| Command |
Payload |
Description |
row:move |
{ rowId, toIndex } |
Move a row to a new display index |
row:swap |
{ rowIdA, rowIdB } |
Swap two rows by their IDs |
Tree Commands
| Command |
Payload |
Description |
tree:toggle |
{ nodeId } |
Toggle a tree node's expanded state |
tree:expand |
{ nodeId } |
Expand a tree node |
tree:collapse |
{ nodeId } |
Collapse a tree node |
tree:expandAll |
{} |
Expand all tree nodes |
tree:collapseAll |
{} |
Collapse all tree nodes |
tree:getNodeState |
{ nodeId } |
Get the state of a tree node |
Server-Side Row Model Commands
| Command |
Payload |
Description |
ssrm:ensureRows |
{ startRow, endRow } |
Ensure rows in the given range are loaded |
ssrm:refresh |
{} |
Refresh server-side data |
ssrm:getCacheInfo |
{} |
Get cache information |
Export Commands
| Command |
Payload |
Description |
excel:exportCsv |
{} |
Export data as CSV |
excel:exportExcel |
{} |
Export data as Excel |
excel:exportData |
{} |
Export raw data |
Master-Detail Commands
| Command |
Payload |
Description |
detail:expand |
{ nodeId } |
Expand a detail row |
detail:collapse |
{ nodeId } |
Collapse a detail row |
detail:toggle |
{ nodeId } |
Toggle a detail row's state |
detail:expandAll |
{} |
Expand all detail rows |
detail:collapseAll |
{} |
Collapse all detail rows |
detail:refreshDetail |
{ nodeId } |
Refresh a specific detail row |
Custom Commands
Plugins can register custom commands via declaration merging:
declare module '@gridstorm/core' {
interface CommandMap {
'myPlugin:doSomething': { value: string };
}
}
// Now type-safe to dispatch
engine.commandBus.dispatch('myPlugin:doSomething', { value: 'hello' });
Next Steps
- Store -- How the store reacts to commands and notifies subscribers.
- Plugin System -- Registering command handlers and event listeners in plugins.
- React Guide -- Event callback props and hooks.