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 { WitTable } from '@witqq/spreadsheet-react';
import type { WitTableRef } from '@witqq/spreadsheet-react';
import { DemoWrapper } from './DemoWrapper';
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<WitTableRef>(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 }}>
<WitTable
theme={witTheme}
ref={tableRef}
columns={sortableColumns}
data={data}
showRowNumbers
editable
style={{ width: '100%', height: '100%' }}
/>
</div>
<div style={{ borderTop: '1px solid #e2e8f0' }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '4px 8px', background: '#f1f5f9', fontSize: 11 }}>
<span style={{ fontWeight: 600 }}>Event Log ({events.length})</span>
<button
onClick={clearLog}
style={{ border: '1px solid #cbd5e1', borderRadius: 4, background: '#fff', padding: '2px 8px', cursor: 'pointer', fontSize: 11 }}
>
Clear
</button>
</div>
<div
ref={logRef}
style={{ height: 120, overflowY: 'auto', background: '#f8fafc', padding: 8, fontFamily: 'monospace', fontSize: 11 }}
>
{events.length === 0 && (
<div style={{ color: '#94a3b8', 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: '#94a3b8' }}>[{evt.time}]</span>{' '}
<span style={{ color: EVENT_COLORS[evt.name] || '#334155', fontWeight: 600 }}>{evt.name}</span>
{': '}
<span style={{ color: '#334155' }}>{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()

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:

<WitTable<Row>
columns={columns}
data={data}
onCellChange={(row, col, oldVal, newVal) => {
// Persist to server
}}
onSelectionChange={(selection) => {
// Update status bar
}}
onSortChange={(columns) => {
// Server-side sort
}}
onFilterChange={(filters) => {
// Server-side filter
}}
onReady={(engine) => {
// Store engine reference, install plugins
}}
/>