Skip to content

Events

All inter-subsystem communication in @witqq/spreadsheet goes through a typed EventBus. It implements a publish/subscribe pattern with type-safe event names and payloads.

Live Demo
Interact with the table — click cells, edit values, sort columns, scroll. Events appear in the log below.
Event Log (0)
No events yet. Click a cell, edit a value, or scroll the table.
View source code
EventBusDemo.tsx
import { useRef, useEffect, useState, useCallback } from 'react';
import { Spreadsheet } from '@witqq/spreadsheet-react';
import type { SpreadsheetRef } from '@witqq/spreadsheet-react';
import { DemoWrapper } from './DemoWrapper';
import { DemoButton } from './DemoButton';
import { generateEmployees, employeeColumns } from './generate-data';
import { useSiteTheme } from './useSiteTheme';
const data = generateEmployees(30);
const sortableColumns = employeeColumns.map(col => ({ ...col, sortable: true }));
const EVENT_COLORS: Record<string, string> = {
cellClick: '#2563eb',
cellChange: '#16a34a',
selectionChange: '#9333ea',
scroll: '#64748b',
sortChange: '#ea580c',
};
interface EventEntry {
time: string;
name: string;
detail: string;
}
export function EventBusDemo() {
const { witTheme } = useSiteTheme();
const tableRef = useRef<SpreadsheetRef>(null);
const logRef = useRef<HTMLDivElement>(null);
const [events, setEvents] = useState<EventEntry[]>([]);
const clearLog = useCallback(() => setEvents([]), []);
useEffect(() => {
const engine = tableRef.current?.getInstance();
if (!engine) return;
const bus = engine.getEventBus();
const logEvent = (name: string, detail: string) => {
const time = new Date().toLocaleTimeString('en', { hour12: false });
setEvents(prev => [...prev.slice(-49), { time, name, detail }]);
};
const unsubs = [
bus.on('cellClick', (e: any) => logEvent('cellClick', `row:${e.row} col:${e.col}`)),
bus.on('cellChange', (e: any) => logEvent('cellChange', `[${e.row},${e.col}] "${e.oldValue}" → "${e.newValue}"`)),
bus.on('selectionChange', (e: any) => logEvent('selectionChange', `row:${e.selection.activeRow} col:${e.selection.activeCol}`)),
bus.on('scroll', (e: any) => logEvent('scroll', `top:${Math.round(e.scrollTop)} left:${Math.round(e.scrollLeft)}`)),
bus.on('sortChange', (e: any) => logEvent('sortChange', `${e.sortColumns.length} column(s)`)),
];
return () => unsubs.forEach(fn => fn());
}, []);
useEffect(() => {
if (logRef.current) {
logRef.current.scrollTop = logRef.current.scrollHeight;
}
}, [events]);
return (
<DemoWrapper height={500} title="Live Demo" description="Interact with the table — click cells, edit values, sort columns, scroll. Events appear in the log below.">
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<div style={{ flex: 1, minHeight: 0 }}>
<Spreadsheet
theme={witTheme}
ref={tableRef}
columns={sortableColumns}
data={data}
showRowNumbers
editable
style={{ width: '100%', height: '100%' }}
/>
</div>
<div style={{ borderTop: '1px solid var(--sl-color-gray-5)' }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '4px 8px', background: 'var(--sl-color-gray-6)', fontSize: 11 }}>
<span style={{ fontWeight: 600, color: 'var(--sl-color-white)' }}>Event Log ({events.length})</span>
<DemoButton onClick={clearLog} style={{ padding: '2px 10px', fontSize: '0.72rem' }}>
Clear
</DemoButton>
</div>
<div
ref={logRef}
style={{ height: 120, overflowY: 'auto', background: 'var(--sl-color-gray-7, var(--sl-color-gray-6))', padding: 8, fontFamily: 'monospace', fontSize: 11 }}
>
{events.length === 0 && (
<div style={{ color: 'var(--sl-color-gray-3)', fontStyle: 'italic' }}>No events yet. Click a cell, edit a value, or scroll the table.</div>
)}
{events.map((evt, i) => (
<div key={i} style={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', lineHeight: '18px' }}>
<span style={{ color: 'var(--sl-color-gray-3)' }}>[{evt.time}]</span>{' '}
<span style={{ color: EVENT_COLORS[evt.name] || 'var(--sl-color-white)', fontWeight: 600 }}>{evt.name}</span>
{': '}
<span style={{ color: 'var(--sl-color-gray-2)' }}>{evt.detail}</span>
</div>
))}
</div>
</div>
</div>
</DemoWrapper>
);
}
const engine = tableRef.current?.getInstance();
// Subscribe to an event
const unsubscribe = engine.on('cellChange', (event) => {
console.log(`Cell [${event.row}, ${event.col}] changed`);
console.log(`Old: ${event.oldValue}, New: ${event.newValue}`);
});
// Unsubscribe when done
unsubscribe();
EventPayloadDescription
cellClick{ row, col, originalEvent }Single click on a cell
cellDoubleClick{ row, col, originalEvent }Double-click on a cell (opens editor)
cellChange{ row, col, oldValue, newValue }Cell value changed after edit commit
cellStatusChange{ row, col, status }Cell status lifecycle: changed → saving → saved, error
cellValidation{ row, col, valid, errors }Validation result after cell edit
EventPayloadDescription
selectionChange{ activeCell, ranges, anchor }Selection changed (click, keyboard, or programmatic)
EventPayloadDescription
scroll{ scrollTop, scrollLeft, viewport }Scroll position changed
EventPayloadDescription
ready{ engine }Engine initialization complete
destroy{}Engine destroyed and resources cleaned up
EventPayloadDescription
commandExecute{ command }A command was executed
commandUndo{ command }A command was undone
commandRedo{ command }A command was redone
EventPayloadDescription
clipboardCopy{ cells, text }Cells copied to clipboard
clipboardCut{ cells, text }Cells cut to clipboard
clipboardPaste{ cells, target }Data pasted from clipboard
EventPayloadDescription
columnResizeStart{ col, width }Column resize drag started
columnResize{ col, width }Column width changing during drag
columnResizeEnd{ col, oldWidth, newWidth }Column resize drag completed
rowResizeStart{ row, height }Row resize drag started
rowResize{ row, height }Row height changing during drag
rowResizeEnd{ row, oldHeight, newHeight }Row resize drag completed
EventPayloadDescription
autofillStart{ startCell, direction }Fill handle drag started
autofillPreview{ range, values }Preview values during drag
autofillComplete{ range, values }Fill operation completed
EventPayloadDescription
sortChange{ columns }Sort configuration changed
sortRejected{ column, reason }Sort request rejected
filterChange{ filters }Filter configuration changed
EventPayloadDescription
rowGroupToggle{ groupKey, expanded }Row group expanded/collapsed
rowGroupChange{ groups }Row grouping configuration changed
EventPayloadDescription
themeChange{ theme }Theme changed via setTheme()

TypeScript interfaces for all event payloads. Import from @witqq/spreadsheet.

Fired on cellClick and cellDoubleClick.

interface CellEvent {
row: number;
col: number;
value: CellValue;
column: ColumnDef;
}

Fired on commandExecute, commandUndo, and commandRedo.

interface CommandEvent {
description: string;
}

Fired on clipboardCopy, clipboardCut, and clipboardPaste.

interface ClipboardDataEvent {
rowCount: number;
colCount: number;
}

Fired on columnResize and columnResizeEnd.

interface ColumnResizeEvent {
colIndex: number;
oldWidth: number;
newWidth: number;
}

Fired on rowResize and rowResizeEnd.

interface RowResizeEvent {
rowIndex: number;
oldHeight: number;
newHeight: number;
}

Fired on cellStatusChange when a cell’s tracking status transitions (e.g. changed → saving → saved).

interface CellStatusChangeEvent {
row: number;
col: number;
oldStatus: CellMetadata['status'] | undefined;
newStatus: CellMetadata['status'] | undefined;
errorMessage?: string;
}

Fired on cellValidation after a cell edit is validated against column/cell rules.

interface CellValidationEvent {
row: number;
col: number;
result: ValidationResult;
}

Fired on internal gridMouseDown, gridMouseMove, gridMouseUp, gridMouseHover, and gridContextMenu.

interface GridMouseEvent extends HitTestResult {
readonly originalEvent: MouseEvent;
readonly shiftKey: boolean;
readonly ctrlKey: boolean;
}

Fired on internal gridKeyDown.

interface GridKeyboardEvent {
readonly originalEvent: KeyboardEvent;
readonly key: string;
readonly shiftKey: boolean;
readonly ctrlKey: boolean;
}

Fired on rowGroupToggle when a row group is expanded or collapsed.

interface RowGroupToggleEvent {
readonly headerRow: number;
readonly expanded: boolean;
}

Fired on rowGroupChange when the set of group headers changes.

interface RowGroupChangeEvent {
readonly groupHeaders: readonly number[];
}

Fired on scroll when the viewport scroll position changes.

interface ScrollEvent {
scrollTop: number;
scrollLeft: number;
}

Fired on sortRejected when a sort request is blocked (e.g. merged regions exist).

interface SortRejectedEvent {
readonly reason: 'merged-regions-exist';
}

Fired on filterChange after filters are applied or cleared.

interface FilterChangeEvent {
readonly visibleRowCount: number;
readonly totalRowCount: number;
}

Fired on autofillStart when the user begins dragging the fill handle.

interface AutofillStartEvent {
sourceRange: CellRange;
}

The EventTranslator converts raw DOM events (mouse clicks, touch, keyboard) into cell-level events. It performs hit-testing to determine which cell or region was interacted with.

Regions identified by EventTranslator:

RegionAreaActions
cellData cells in the grid bodyClick, edit, select
headerColumn header rowSort toggle, filter open, resize
row-numberRow number column on the leftRow selection
cornerTop-left corner (row numbers × header)Select all

On touch devices, the EventTranslator maps gestures:

GestureAction
TapSelect cell
Double-tapOpen inline editor
ScrollNative scroll (CSS touch-action: pan-x pan-y)

In the React wrapper, events are exposed as props:

<Spreadsheet<Row>
columns={columns}
data={data}
onCellChange={(event: CellChangeEvent) => {
// event.row, event.col, event.oldValue, event.newValue
}}
onSelectionChange={(event: SelectionChangeEvent) => {
// event.selection
}}
onSortChange={(event: SortChangeEvent) => {
// event.columns
}}
onFilterChange={(event: FilterChangeEvent) => {
// event.filters
}}
onReady={() => {
// Table mounted and ready
}}
/>