Editable Grid
Grid with inline editing, multiple editor types, validation, and undo/redo.
This example demonstrates how to enable inline editing with various editor types, implement validation, and use undo/redo functionality.
Enable Column Editing
Add editable: true and specify an editor type for each column:
import { createGrid } from '@zengrid/core';import type { Column, GridOptions } from '@zengrid/core';
const columns: Column[] = [ { id: 'id', header: 'ID', width: 60, // ID is not editable }, { id: 'name', header: 'Name', width: 200, editable: true, editor: 'text', // Text input }, { id: 'email', header: 'Email', width: 250, editable: true, editor: 'text', }, { id: 'age', header: 'Age', width: 100, editable: true, editor: 'number', // Number input }, { id: 'active', header: 'Active', width: 100, editable: true, editor: 'checkbox', // Checkbox },];
const grid = createGrid(container, { columns, rowCount: 100, rowHeight: 40, data: (rowIndex, colId) => { // Data callback implementation return dataStore[rowIndex]?.[colId]; },});Double-click a cell or press Enter to start editing. Press Enter to commit changes, or Escape to cancel.
Editor Types
ZenGrid supports multiple built-in editor types:
Text Editor
{ id: 'name', header: 'Name', width: 200, editable: true, editor: 'text', editorParams: { placeholder: 'Enter name...', maxLength: 100, },}Number Editor
{ id: 'age', header: 'Age', width: 100, editable: true, editor: 'number', editorParams: { min: 0, max: 120, step: 1, placeholder: '0', },}Checkbox Editor
{ id: 'active', header: 'Active', width: 100, editable: true, editor: 'checkbox', editorParams: { checkedValue: true, uncheckedValue: false, },}Date Editor
{ id: 'birthdate', header: 'Birth Date', width: 150, editable: true, editor: 'date', editorParams: { format: 'yyyy-MM-dd', min: '1900-01-01', max: '2025-12-31', },}Select Editor
{ id: 'role', header: 'Role', width: 150, editable: true, editor: 'select', editorParams: { options: [ { value: 'admin', label: 'Administrator' }, { value: 'editor', label: 'Editor' }, { value: 'viewer', label: 'Viewer' }, ], },}Dropdown Editor
{ id: 'country', header: 'Country', width: 150, editable: true, editor: 'dropdown', editorParams: { options: [ 'United States', 'United Kingdom', 'Canada', 'Australia', 'Germany', 'France', ], allowCustomValue: false, searchable: true, },}Use select for a small fixed list of options, and dropdown for a larger searchable list.
Validation
Add validation to columns to ensure data integrity:
import type { Column } from '@zengrid/core';
const columns: Column[] = [ { id: 'email', header: 'Email', width: 250, editable: true, editor: 'text', validator: (value: string) => { const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!emailRegex.test(value)) { return 'Invalid email format'; } return null; // Valid }, }, { id: 'age', header: 'Age', width: 100, editable: true, editor: 'number', validator: (value: number) => { if (value < 18) { return 'Must be at least 18 years old'; } if (value > 120) { return 'Invalid age'; } return null; // Valid }, }, { id: 'username', header: 'Username', width: 150, editable: true, editor: 'text', validator: async (value: string) => { // Async validation (e.g., check availability) const response = await fetch(`/api/check-username?username=${value}`); const data = await response.json();
if (!data.available) { return 'Username already taken'; } return null; // Valid }, },];Validation errors prevent the edit from being committed. The error message is displayed to the user.
Edit Events
Handle edit events to update data and trigger side effects:
// Before edit starts (can cancel)grid.on('edit:start', (event) => { console.log('Edit started:', event.rowIndex, event.colId);
// Cancel edit conditionally if (event.rowIndex === 0) { event.preventDefault(); }});
// Edit value changedgrid.on('edit:change', (event) => { console.log('Edit changed:', event.value);});
// Edit committedgrid.on('edit:commit', (event) => { console.log('Edit committed:', { rowIndex: event.rowIndex, colId: event.colId, oldValue: event.oldValue, newValue: event.newValue, });
// Update data store dataStore[event.rowIndex][event.colId] = event.newValue;
// Trigger save to backend saveToBackend(event.rowIndex, event.colId, event.newValue);});
// Edit cancelledgrid.on('edit:cancel', (event) => { console.log('Edit cancelled:', event.rowIndex, event.colId);});
// Validation errorgrid.on('edit:validation-error', (event) => { console.error('Validation error:', event.error);
// Show custom error UI showErrorToast(event.error);});Undo/Redo
Enable undo/redo functionality for edit operations:
import { createGrid, UndoRedoManager } from '@zengrid/core';
const grid = createGrid(container, options);
// Create undo/redo managerconst undoRedo = new UndoRedoManager(grid, { maxHistorySize: 50, // Keep last 50 actions});
// Undo last editundoRedo.undo();
// Redo last undone editundoRedo.redo();
// Check if undo/redo availableconsole.log('Can undo:', undoRedo.canUndo());console.log('Can redo:', undoRedo.canRedo());
// Clear historyundoRedo.clear();
// Listen to undo/redo eventsundoRedo.on('undo', (event) => { console.log('Undone:', event.action);});
undoRedo.on('redo', (event) => { console.log('Redone:', event.action);});
// Keyboard shortcutsdocument.addEventListener('keydown', (e) => { if (e.ctrlKey && e.key === 'z') { e.preventDefault(); undoRedo.undo(); } if (e.ctrlKey && e.key === 'y') { e.preventDefault(); undoRedo.redo(); }});UndoRedoManager automatically tracks all edit operations. You don’t need to manually record changes.
Complete Editable Example
import { createGrid, UndoRedoManager } from '@zengrid/core';import type { Column, GridOptions } from '@zengrid/core';
// Data storeconst dataStore = [ { id: 1, name: 'Alice', email: 'alice@example.com', age: 30, role: 'admin', active: true }, { id: 2, name: 'Bob', email: 'bob@example.com', age: 25, role: 'editor', active: true }, { id: 3, name: 'Charlie', email: 'charlie@example.com', age: 35, role: 'viewer', active: false }, // ... more rows];
// Column definitions with various editorsconst columns: Column[] = [ { id: 'id', header: 'ID', width: 60, }, { id: 'name', header: 'Name', width: 200, editable: true, editor: 'text', editorParams: { placeholder: 'Enter name...', }, validator: (value: string) => { if (value.trim().length < 2) { return 'Name must be at least 2 characters'; } return null; }, }, { id: 'email', header: 'Email', width: 250, editable: true, editor: 'text', validator: (value: string) => { const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!emailRegex.test(value)) { return 'Invalid email format'; } return null; }, }, { id: 'age', header: 'Age', width: 100, editable: true, editor: 'number', editorParams: { min: 18, max: 120, }, }, { id: 'role', header: 'Role', width: 150, editable: true, editor: 'select', editorParams: { options: [ { value: 'admin', label: 'Administrator' }, { value: 'editor', label: 'Editor' }, { value: 'viewer', label: 'Viewer' }, ], }, }, { id: 'active', header: 'Active', width: 100, editable: true, editor: 'checkbox', },];
// Create gridconst grid = createGrid(container, { columns, rowCount: dataStore.length, rowHeight: 40, data: (rowIndex, colId) => { return dataStore[rowIndex]?.[colId] ?? ''; },});
// Setup undo/redoconst undoRedo = new UndoRedoManager(grid, { maxHistorySize: 50,});
// Handle edit commitsgrid.on('edit:commit', (event) => { // Update data store dataStore[event.rowIndex][event.colId] = event.newValue;
// Save to backend console.log('Saving to backend:', { id: dataStore[event.rowIndex].id, field: event.colId, value: event.newValue, });});
// Add undo/redo buttonsconst toolbar = document.createElement('div');toolbar.innerHTML = ` <button id="undo">Undo</button> <button id="redo">Redo</button>`;document.body.prepend(toolbar);
document.getElementById('undo')?.addEventListener('click', () => { undoRedo.undo();});
document.getElementById('redo')?.addEventListener('click', () => { undoRedo.redo();});
// Keyboard shortcutsdocument.addEventListener('keydown', (e) => { if (e.ctrlKey && e.key === 'z') { e.preventDefault(); undoRedo.undo(); } if (e.ctrlKey && e.key === 'y') { e.preventDefault(); undoRedo.redo(); }});Editing works seamlessly with sorting and filtering. Edits are applied to the underlying data, not the sorted/filtered view.
Performance Considerations
- Validation functions should be fast; use debouncing for async validation
- UndoRedoManager keeps actions in memory; limit history size for large grids
- Batch multiple edits when possible to reduce event overhead
Next Steps
- Combine with sorting and filtering
- Learn about custom editors
- Explore form integration patterns