Plugin Architecture
Plugin Architecture
Section titled “Plugin Architecture”witqq spreadsheet uses a lightweight plugin system that lets you extend the engine without modifying core code. Plugins can add render layers, listen to events, store isolated state, and integrate with undo/redo.
View source code
import { useRef, useEffect, useState, useCallback } from 'react';import { WitTable } from '@witqq/spreadsheet-react';import type { WitTableRef } from '@witqq/spreadsheet-react';import type { ColumnDef } from '@witqq/spreadsheet';import { FormulaPlugin, ConditionalFormattingPlugin } from '@witqq/spreadsheet-plugins';import { DemoWrapper } from './DemoWrapper';import { useSiteTheme } from './useSiteTheme';
const columns: ColumnDef[] = [ { key: 'a', title: 'Value A', width: 100, type: 'number' }, { key: 'b', title: 'Value B', width: 100, type: 'number' }, { key: 'c', title: 'Sum (A+B)', width: 120 }, { key: 'score', title: 'Score', width: 100, type: 'number' },];
const initialData = [ { a: 10, b: 20, c: '', score: 85 }, { a: 25, b: 15, c: '', score: 42 }, { a: 30, b: 10, c: '', score: 95 }, { a: 5, b: 45, c: '', score: 28 }, { a: 15, b: 35, c: '', score: 73 }, { a: 40, b: 20, c: '', score: 58 }, { a: 20, b: 30, c: '', score: 91 }, { a: 35, b: 5, c: '', score: 15 }, { a: 8, b: 42, c: '', score: 67 }, { a: 50, b: 10, c: '', score: 100 },];
function ToggleButton({ label, enabled, onClick,}: { label: string; enabled: boolean; onClick: () => void;}) { return ( <button onClick={onClick} style={{ padding: '4px 12px', borderRadius: '12px', border: '1px solid', borderColor: enabled ? '#22c55e' : '#d1d5db', backgroundColor: enabled ? '#dcfce7' : '#f9fafb', color: enabled ? '#15803d' : '#6b7280', cursor: 'pointer', fontSize: '0.8rem', fontWeight: 500, }} > {label}: {enabled ? 'ON' : 'OFF'} </button> );}
export function PluginShowcaseDemo() { const { witTheme } = useSiteTheme(); const tableRef = useRef<WitTableRef>(null); const [formulaEnabled, setFormulaEnabled] = useState(false); const [condFormatEnabled, setCondFormatEnabled] = useState(false); const formulaPluginRef = useRef<FormulaPlugin | null>(null); const condFormatPluginRef = useRef<ConditionalFormattingPlugin | null>(null);
const toggleFormula = useCallback(() => { const engine = tableRef.current?.getInstance(); if (!engine) return;
if (formulaEnabled) { engine.removePlugin('formula'); formulaPluginRef.current = null; for (let row = 0; row < 10; row++) { engine.setCell(row, 2, ''); } engine.requestRender(); } else { const plugin = new FormulaPlugin({ syncOnly: true }); engine.installPlugin(plugin); formulaPluginRef.current = plugin; for (let row = 0; row < 10; row++) { const formula = `=A${row + 1}+B${row + 1}`; engine.setCell(row, 2, formula); engine.getEventBus().emit('cellChange', { row, col: 2, value: formula, column: columns[2], oldValue: '', newValue: formula, source: 'edit' as const, }); } engine.requestRender(); } setFormulaEnabled(!formulaEnabled); }, [formulaEnabled]);
const toggleCondFormat = useCallback(() => { const engine = tableRef.current?.getInstance(); if (!engine) return;
if (condFormatEnabled) { engine.removePlugin('conditional-format'); condFormatPluginRef.current = null; engine.requestRender(); } else { const plugin = new ConditionalFormattingPlugin(); engine.installPlugin(plugin); plugin.addRule( ConditionalFormattingPlugin.createGradientScale( { startRow: 0, startCol: 3, endRow: 9, endCol: 3 }, [ { value: 0, color: '#ef4444' }, { value: 50, color: '#eab308' }, { value: 100, color: '#22c55e' }, ] ) ); condFormatPluginRef.current = plugin; engine.requestRender(); } setCondFormatEnabled(!condFormatEnabled); }, [condFormatEnabled]);
return ( <DemoWrapper height={440} title="Live Demo" description="Toggle plugins on and off to see their effects. Formula calculates Sum column. Conditional formatting applies gradient to Score." > <div style={{ display: 'flex', gap: '8px', marginBottom: '8px' }}> <ToggleButton label="Formula Plugin" enabled={formulaEnabled} onClick={toggleFormula} /> <ToggleButton label="Conditional Formatting" enabled={condFormatEnabled} onClick={toggleCondFormat} /> </div> <WitTable theme={witTheme} ref={tableRef} columns={columns} data={initialData} editable={true} showRowNumbers={true} /> </DemoWrapper> );}WitPlugin Interface
Section titled “WitPlugin Interface”Every plugin implements WitPlugin:
interface WitPlugin { readonly name: string; readonly version: string; readonly dependencies?: string[]; install(api: PluginAPI): void; destroy?(): void;}| Field | Description |
|---|---|
name | Unique identifier (e.g. 'formula', 'conditional-format') |
version | Semver string |
dependencies | Optional array of plugin names that must be installed first |
install(api) | Called when the plugin is added to the engine |
destroy() | Optional cleanup — unbind events, remove layers |
PluginAPI
Section titled “PluginAPI”The install method receives a PluginAPI object:
interface PluginAPI { readonly engine: WitEngine; getPluginState<T>(key: string): T | undefined; setPluginState<T>(key: string, value: T): void;}engine— full access toWitEngine(event bus, cell store, render pipeline, etc.)getPluginState / setPluginState— isolated key-value storage per plugin, managed by the engine
Installing and Removing Plugins
Section titled “Installing and Removing Plugins”Direct engine access
Section titled “Direct engine access”import { WitEngine } from '@witqq/spreadsheet';
const engine = new WitEngine(config);engine.installPlugin(myPlugin);engine.removePlugin('my-plugin');Via React ref
Section titled “Via React ref”const tableRef = useRef<WitTableRef>(null);
// After mounttableRef.current?.installPlugin(myPlugin);tableRef.current?.removePlugin('my-plugin');Official Plugins
Section titled “Official Plugins”| Plugin | Package | Description |
|---|---|---|
FormulaPlugin | @witqq/spreadsheet-plugins | Spreadsheet formulas with dependency graph |
ConditionalFormattingPlugin | @witqq/spreadsheet-plugins | Color scales, data bars, icon sets |
ExcelPlugin | @witqq/spreadsheet-plugins | Import/export .xlsx via lazy-loaded SheetJS |
createContextMenuPlugin | @witqq/spreadsheet-plugins | Right-click context menu with custom items |
CollaborationPlugin | @witqq/spreadsheet-plugins | Real-time OT collaboration with remote cursors |
ProgressiveLoaderPlugin | @witqq/spreadsheet-plugins | Non-blocking large dataset loading with progress overlay |
Custom Plugin Example
Section titled “Custom Plugin Example”A plugin that highlights the active row:
import type { WitPlugin, PluginAPI } from '@witqq/spreadsheet';
const activeRowPlugin: WitPlugin = { name: 'active-row-highlight', version: '1.0.0',
install(api: PluginAPI) { const { engine } = api;
const handler = (event: { selection: Selection }) => { const row = event.selection.activeCell.row; api.setPluginState('activeRow', row); engine.requestRender(); };
engine.on('selectionChange', handler); api.setPluginState('handler', handler); },
destroy() { // Cleanup handled by engine when plugin is removed },};Installing Plugins in React
Section titled “Installing Plugins in React”import { WitTable, WitTableRef } from '@witqq/spreadsheet-react';import { FormulaPlugin, ConditionalFormattingPlugin } from '@witqq/spreadsheet-plugins';
function App() { const ref = useRef<WitTableRef>(null);
useEffect(() => { ref.current?.installPlugin(new FormulaPlugin()); ref.current?.installPlugin(new ConditionalFormattingPlugin()); return () => { ref.current?.removePlugin('formula'); ref.current?.removePlugin('conditional-format'); }; }, []);
return <WitTable ref={ref} columns={columns} data={data} />;}