Skip to content

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.

Live Demo
Toggle plugins on and off to see their effects. Formula calculates Sum column. Conditional formatting applies gradient to Score.
View source code
PluginShowcaseDemo.tsx
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>
);
}

Every plugin implements WitPlugin:

interface WitPlugin {
readonly name: string;
readonly version: string;
readonly dependencies?: string[];
install(api: PluginAPI): void;
destroy?(): void;
}
FieldDescription
nameUnique identifier (e.g. 'formula', 'conditional-format')
versionSemver string
dependenciesOptional 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

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 to WitEngine (event bus, cell store, render pipeline, etc.)
  • getPluginState / setPluginState — isolated key-value storage per plugin, managed by the engine
import { WitEngine } from '@witqq/spreadsheet';
const engine = new WitEngine(config);
engine.installPlugin(myPlugin);
engine.removePlugin('my-plugin');
const tableRef = useRef<WitTableRef>(null);
// After mount
tableRef.current?.installPlugin(myPlugin);
tableRef.current?.removePlugin('my-plugin');
PluginPackageDescription
FormulaPlugin@witqq/spreadsheet-pluginsSpreadsheet formulas with dependency graph
ConditionalFormattingPlugin@witqq/spreadsheet-pluginsColor scales, data bars, icon sets
ExcelPlugin@witqq/spreadsheet-pluginsImport/export .xlsx via lazy-loaded SheetJS
createContextMenuPlugin@witqq/spreadsheet-pluginsRight-click context menu with custom items
CollaborationPlugin@witqq/spreadsheet-pluginsReal-time OT collaboration with remote cursors
ProgressiveLoaderPlugin@witqq/spreadsheet-pluginsNon-blocking large dataset loading with progress overlay

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
},
};
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} />;
}