Creating Custom Plugins
Step-by-step tutorial for creating custom ZenGrid plugins with store access and signal ownership.
This tutorial walks through creating a custom ZenGrid plugin step-by-step. We’ll build a RowHighlighterPlugin that highlights rows based on custom criteria.
Plugin Requirements
Every ZenGrid plugin must:
- Implement the
GridPlugininterface - Choose an appropriate phase number
- Declare dependencies (if any)
- Implement the
setupmethod - Return a
PluginDisposablefor cleanup - Clean up resources in
dispose(optional)
Step 1: Define the Plugin Interface
import { GridPlugin, GridStore, GridApi, PluginDisposable } from '@zengrid/core';
const RowHighlighterPlugin: GridPlugin = { name: 'RowHighlighterPlugin', phase: 60, dependencies: ['CorePlugin', 'SelectionPlugin'],
setup(store: GridStore, api: GridApi): PluginDisposable { // Plugin implementation return { teardown: [] }; },
dispose() { // Optional cleanup }};Choose a phase number higher than your dependencies to ensure proper initialization order.
Step 2: Choose the Phase
Phase determines initialization order:
const RowHighlighterPlugin: GridPlugin = { name: 'RowHighlighterPlugin', phase: 60, // After CorePlugin(0), SelectionPlugin(40), EditingPlugin(45), UndoRedoPlugin(50) dependencies: ['CorePlugin', 'SelectionPlugin']};Step 3: Extend the Store
Add custom signals to the reactive store:
setup(store: GridStore, api: GridApi): PluginDisposable { // Extend store with custom signals store.extend('highlighter.enabled', true, 'RowHighlighterPlugin', this.phase); store.extend('highlighter.color', '#ffeb3b', 'RowHighlighterPlugin', this.phase); store.extend('highlighter.rows', new Set<number>(), 'RowHighlighterPlugin', this.phase);
return { teardown: [] };}Use store.extend() to create new signals owned by your plugin. The store automatically cleans them up on plugin disposal.
Step 4: Create Computed Signals
Add derived state using computed signals:
setup(store: GridStore, api: GridApi): PluginDisposable { // Extend store store.extend('highlighter.enabled', true, 'RowHighlighterPlugin', this.phase); store.extend('highlighter.rows', new Set<number>(), 'RowHighlighterPlugin', this.phase);
// Create computed signal store.computed('highlighter.count', () => { const rows = store.get('highlighter.rows'); return rows.size; }, 'RowHighlighterPlugin', this.phase);
store.computed('highlighter.hasHighlights', () => { const count = store.get('highlighter.count'); return count > 0; }, 'RowHighlighterPlugin', this.phase);
return { teardown: [] };}Step 5: Create Effects
React to store changes using effects:
setup(store: GridStore, api: GridApi): PluginDisposable { // ... previous code ...
// Create effect to update UI when rows change store.effect('updateHighlightUI', () => { const enabled = store.get('highlighter.enabled'); const rows = store.get('highlighter.rows'); const color = store.get('highlighter.color');
if (enabled) { this.applyHighlights(rows, color); } else { this.clearHighlights(); } }, 'RowHighlighterPlugin', this.phase);
return { teardown: [] };}
private applyHighlights(rows: Set<number>, color: string): void { rows.forEach(row => { const rowElement = document.querySelector(`[data-row="${row}"]`); if (rowElement) { (rowElement as HTMLElement).style.backgroundColor = color; } });}
private clearHighlights(): void { const highlightedRows = document.querySelectorAll('[style*="background-color"]'); highlightedRows.forEach(row => { (row as HTMLElement).style.backgroundColor = ''; });}Effects run automatically when their dependencies change. Be careful not to create infinite loops.
Step 6: Register API Methods
Expose plugin functionality through the grid API:
setup(store: GridStore, api: GridApi): PluginDisposable { // ... previous code ...
// Register API methods api.register('highlighter', { enable: () => { store.set('highlighter.enabled', true); },
disable: () => { store.set('highlighter.enabled', false); },
setColor: (color: string) => { store.set('highlighter.color', color); },
highlightRow: (row: number) => { const rows = new Set(store.get('highlighter.rows')); rows.add(row); store.set('highlighter.rows', rows); },
unhighlightRow: (row: number) => { const rows = new Set(store.get('highlighter.rows')); rows.delete(row); store.set('highlighter.rows', rows); },
clearHighlights: () => { store.set('highlighter.rows', new Set()); },
getHighlightedRows: () => { return Array.from(store.get('highlighter.rows')); } });
return { teardown: [] };}Step 7: Add Cleanup
Return cleanup functions in the PluginDisposable:
setup(store: GridStore, api: GridApi): PluginDisposable { // ... previous code ...
// Add event listeners const handleRowClick = (event: MouseEvent) => { const row = this.getRowFromEvent(event); if (row !== null && event.ctrlKey) { api.highlighter.highlightRow(row); } };
document.addEventListener('click', handleRowClick);
// Return disposable with cleanup return { teardown: [ () => { document.removeEventListener('click', handleRowClick); }, () => { this.clearHighlights(); } ] };}The teardown array is executed in order when the plugin is destroyed.
Step 8: Register the Plugin
Use the plugin with your grid:
import { createGrid } from '@zengrid/core';import { RowHighlighterPlugin } from './row-highlighter-plugin';
const grid = createGrid(container, { columns: [...], data: [...]});
// Register the plugingrid.usePlugin(RowHighlighterPlugin);
// Use the plugin APIgrid.api.highlighter.setColor('#ffeb3b');grid.api.highlighter.highlightRow(5);grid.api.highlighter.highlightRow(10);
// Get highlighted rowsconst highlighted = grid.api.highlighter.getHighlightedRows();console.log('Highlighted rows:', highlighted);Step 9: Optional Dispose Method
Add a dispose method for additional cleanup:
const RowHighlighterPlugin: GridPlugin = { name: 'RowHighlighterPlugin', phase: 60, dependencies: ['CorePlugin', 'SelectionPlugin'],
setup(store: GridStore, api: GridApi): PluginDisposable { // ... setup code ... return { teardown: [] }; },
dispose() { // Optional: cleanup that happens after teardown console.log('RowHighlighterPlugin disposed'); }};Complete Working Example
Here’s the complete plugin implementation:
import { GridPlugin, GridStore, GridApi, PluginDisposable } from '@zengrid/core';
export const RowHighlighterPlugin: GridPlugin = { name: 'RowHighlighterPlugin', phase: 60, dependencies: ['CorePlugin', 'SelectionPlugin'],
setup(store: GridStore, api: GridApi): PluginDisposable { // 1. Extend store with custom signals store.extend('highlighter.enabled', true, 'RowHighlighterPlugin', this.phase); store.extend('highlighter.color', '#ffeb3b', 'RowHighlighterPlugin', this.phase); store.extend('highlighter.rows', new Set<number>(), 'RowHighlighterPlugin', this.phase);
// 2. Create computed signals store.computed('highlighter.count', () => { const rows = store.get('highlighter.rows'); return rows.size; }, 'RowHighlighterPlugin', this.phase);
store.computed('highlighter.hasHighlights', () => { const count = store.get('highlighter.count'); return count > 0; }, 'RowHighlighterPlugin', this.phase);
// 3. Create effects store.effect('updateHighlightUI', () => { const enabled = store.get('highlighter.enabled'); const rows = store.get('highlighter.rows'); const color = store.get('highlighter.color');
if (enabled) { applyHighlights(rows, color); } else { clearHighlights(); } }, 'RowHighlighterPlugin', this.phase);
// 4. Register API methods api.register('highlighter', { enable: () => store.set('highlighter.enabled', true), disable: () => store.set('highlighter.enabled', false), setColor: (color: string) => store.set('highlighter.color', color),
highlightRow: (row: number) => { const rows = new Set(store.get('highlighter.rows')); rows.add(row); store.set('highlighter.rows', rows); },
unhighlightRow: (row: number) => { const rows = new Set(store.get('highlighter.rows')); rows.delete(row); store.set('highlighter.rows', rows); },
clearHighlights: () => store.set('highlighter.rows', new Set()), getHighlightedRows: () => Array.from(store.get('highlighter.rows')) });
// 5. Add event listeners const handleRowClick = (event: MouseEvent) => { const target = event.target as HTMLElement; const rowElement = target.closest('[data-row]'); if (rowElement && event.ctrlKey) { const row = parseInt(rowElement.getAttribute('data-row')!, 10); api.highlighter.highlightRow(row); } };
document.addEventListener('click', handleRowClick);
// 6. Return disposable with cleanup return { teardown: [ () => document.removeEventListener('click', handleRowClick), () => clearHighlights() ] }; },
dispose() { console.log('RowHighlighterPlugin disposed'); }};
// Helper functionsfunction applyHighlights(rows: Set<number>, color: string): void { rows.forEach(row => { const rowElement = document.querySelector(`[data-row="${row}"]`); if (rowElement) { (rowElement as HTMLElement).style.backgroundColor = color; } });}
function clearHighlights(): void { const highlightedRows = document.querySelectorAll('[style*="background-color"]'); highlightedRows.forEach(row => { (row as HTMLElement).style.backgroundColor = ''; });}Usage Example
import { createGrid } from '@zengrid/core';import { RowHighlighterPlugin } from './row-highlighter-plugin';
const grid = createGrid(document.getElementById('grid')!, { columns: [ { id: 'name', field: 'name', header: 'Name' }, { id: 'age', field: 'age', header: 'Age' } ], data: [ { name: 'Alice', age: 25 }, { name: 'Bob', age: 30 }, { name: 'Charlie', age: 35 } ]});
grid.usePlugin(RowHighlighterPlugin);
// Highlight rowsgrid.api.highlighter.highlightRow(0);grid.api.highlighter.highlightRow(2);
// Change colorgrid.api.highlighter.setColor('#4caf50');
// Get highlighted rowsconsole.log(grid.api.highlighter.getHighlightedRows()); // [0, 2]
// Clear highlightsgrid.api.highlighter.clearHighlights();This plugin demonstrates all key concepts: store integration, computed signals, effects, API registration, and cleanup.