Editing & Clipboard
OGrid supports inline cell editing, clipboard copy/paste, drag-to-fill, and undo/redo -- all working together through onCellValueChanged and valueParser.
Live Demo
The demo above uses Radix UI for styling. To see this feature with your framework's design system (Fluent UI, Material UI, Vuetify, PrimeNG, etc.), click "Open in online demo" below the demo.
Quick Example
- React
- Angular
- Vue
- Vanilla JS
import { OGrid, useUndoRedo } from '@alaarab/ogrid-react-radix';
import { useState } from 'react';
const DEPTS = ['Engineering', 'Marketing', 'Sales', 'Finance', 'Operations'];
const STATUSES = ['Active', 'Draft', 'Archived'];
const columns = [
{ columnId: 'name', name: 'Name', editable: true },
{
columnId: 'department',
name: 'Department',
editable: true,
cellEditor: 'richSelect',
cellEditorParams: { values: DEPTS },
},
{
columnId: 'status',
name: 'Status',
editable: true,
cellEditor: 'select',
cellEditorParams: { values: STATUSES },
},
{
columnId: 'salary',
name: 'Salary',
editable: true,
type: 'numeric',
valueParser: ({ newValue }) => {
const num = Number(newValue);
return isNaN(num) || num < 0 ? undefined : num; // reject invalid
},
valueFormatter: (v) => `$${Number(v).toLocaleString()}`,
},
];
function App() {
const [data, setData] = useState(initialData);
const { handleCellValueChanged, undo, redo, canUndo, canRedo } = useUndoRedo({
data, setData, getRowId: (item) => item.id,
});
return (
<OGrid
columns={columns}
data={data}
getRowId={(item) => item.id}
editable
onCellValueChanged={handleCellValueChanged}
onUndo={undo}
onRedo={redo}
canUndo={canUndo}
canRedo={canRedo}
/>
);
}
The OGrid component has the same props across all React UI packages. To switch, just change the import:
- Radix (lightweight, default):
from '@alaarab/ogrid-react-radix' - Fluent UI (Microsoft 365 / SPFx):
from '@alaarab/ogrid-react-fluent'— wrap in<FluentProvider> - Material UI (MUI v7):
from '@alaarab/ogrid-react-material'— wrap in<ThemeProvider>
import { Component } from '@angular/core';
import { OGridComponent, type IColumnDef } from '@alaarab/ogrid-angular-material';
const DEPTS = ['Engineering', 'Marketing', 'Sales', 'Finance', 'Operations'];
const STATUSES = ['Active', 'Draft', 'Archived'];
@Component({
standalone: true,
imports: [OGridComponent],
template: `<ogrid [props]="gridProps" />`
})
export class GridComponent {
gridProps = {
columns: [
{ columnId: 'name', name: 'Name', editable: true },
{
columnId: 'department', name: 'Department', editable: true,
cellEditor: 'richSelect', cellEditorParams: { values: DEPTS },
},
{
columnId: 'status', name: 'Status', editable: true,
cellEditor: 'select', cellEditorParams: { values: STATUSES },
},
{
columnId: 'salary', name: 'Salary', editable: true, type: 'numeric',
valueParser: ({ newValue }: any) => {
const num = Number(newValue);
return isNaN(num) || num < 0 ? undefined : num;
},
valueFormatter: (v: unknown) => `$${Number(v).toLocaleString()}`,
},
] as IColumnDef<Person>[],
data: initialData,
getRowId: (item: Person) => item.id,
editable: true,
};
}
Same component API across Angular packages. To switch, just change the import:
- Radix (CDK):
from '@alaarab/ogrid-angular-radix'(default, lightweight) - Angular Material:
from '@alaarab/ogrid-angular-material' - PrimeNG:
from '@alaarab/ogrid-angular-primeng'
All components are standalone — no NgModule required.
<script setup lang="ts">
import { ref } from 'vue';
import { OGrid, type IColumnDef } from '@alaarab/ogrid-vue-vuetify';
const DEPTS = ['Engineering', 'Marketing', 'Sales', 'Finance', 'Operations'];
const STATUSES = ['Active', 'Draft', 'Archived'];
const columns: IColumnDef<Person>[] = [
{ columnId: 'name', name: 'Name', editable: true },
{
columnId: 'department', name: 'Department', editable: true,
cellEditor: 'richSelect', cellEditorParams: { values: DEPTS },
},
{
columnId: 'status', name: 'Status', editable: true,
cellEditor: 'select', cellEditorParams: { values: STATUSES },
},
{
columnId: 'salary', name: 'Salary', editable: true, type: 'numeric',
valueParser: ({ newValue }) => {
const num = Number(newValue);
return isNaN(num) || num < 0 ? undefined : num;
},
valueFormatter: (v) => `$${Number(v).toLocaleString()}`,
},
];
const gridProps = {
columns,
data: initialData,
getRowId: (item) => item.id,
editable: true,
};
</script>
<template>
<OGrid :gridProps="gridProps" />
</template>
Same component API across Vue packages. To switch, just change the import:
- Radix (Headless UI):
from '@alaarab/ogrid-vue-radix'(default, lightweight) - Vuetify:
from '@alaarab/ogrid-vue-vuetify'— wrap in<v-app>for theming - PrimeVue:
from '@alaarab/ogrid-vue-primevue'
import { OGrid } from '@alaarab/ogrid-js';
import '@alaarab/ogrid-js/styles';
const DEPTS = ['Engineering', 'Marketing', 'Sales', 'Finance', 'Operations'];
const STATUSES = ['Active', 'Draft', 'Archived'];
const grid = new OGrid(document.getElementById('grid'), {
columns: [
{ columnId: 'name', name: 'Name', editable: true },
{
columnId: 'department',
name: 'Department',
editable: true,
cellEditor: 'richSelect',
cellEditorParams: { values: DEPTS },
},
{
columnId: 'status',
name: 'Status',
editable: true,
cellEditor: 'select',
cellEditorParams: { values: STATUSES },
},
{
columnId: 'salary',
name: 'Salary',
editable: true,
type: 'numeric',
valueParser: ({ newValue }) => {
const num = Number(newValue);
return isNaN(num) || num < 0 ? undefined : num;
},
valueFormatter: (v) => `$${Number(v).toLocaleString()}`,
},
],
data: initialData,
getRowId: (item) => item.id,
editable: true,
});
// Listen for cell edits
grid.on('cellValueChanged', (event) => {
console.log(`${event.columnId} changed to ${event.newValue}`);
});
This single setup gives you inline editing, clipboard paste, fill handle, and full undo/redo.
Inline Editing
Double-click or press F2 to edit. Enter commits and moves down, Tab commits and moves right, Escape cancels.
Built-in Editors
cellEditor | Description |
|---|---|
'text' (default) | Standard text input |
'select' | Dropdown from cellEditorParams.values |
'checkbox' | Toggle boolean |
'richSelect' | Searchable dropdown with keyboard nav |
Rich Select
{
columnId: 'category',
editable: true,
cellEditor: 'richSelect',
cellEditorParams: {
values: ['Engineering', 'Design', 'Marketing', 'Sales'],
formatValue: (v) => `Dept: ${v}`,
},
}
Per-Row Editability
{ columnId: 'name', editable: (item) => item.status !== 'locked' }
Custom Editor
Pass a component to cellEditor. Use cellEditorPopup: true for a popover:
function DateEditor({ value, onValueChange, onCommit, onCancel }: ICellEditorProps<Task>) {
return (
<input
type="date"
value={value as string}
onChange={(e) => onValueChange(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') onCommit();
if (e.key === 'Escape') onCancel();
}}
autoFocus
/>
);
}
// In column def:
{ columnId: 'dueDate', editable: true, cellEditor: DateEditor, cellEditorPopup: true }
Value Parsing
valueParser validates input for editing, paste, fill, and delete. Return undefined to reject:
{ columnId: 'age', editable: true, type: 'numeric',
valueParser: ({ newValue }) => {
const num = Number(newValue);
return isNaN(num) || num < 0 || num > 150 ? undefined : num;
},
}
Clipboard
Standard keyboard shortcuts on selected cells:
| Key | Action | Requires editable |
|---|---|---|
| Ctrl+C (Cmd+C) | Copy as tab-delimited text | No |
| Ctrl+V (Cmd+V) | Paste into cells | Yes |
| Ctrl+X (Cmd+X) | Cut (copy + clear) | Yes |
| Delete / Backspace | Clear selected cells | Yes |
- Copy uses
valueFormatterso copied values match the display. - Paste calls
valueParseron each value and firesonCellValueChangedper cell.
Fill Handle
A small square at the bottom-right of the selection. Drag it down to fill cells with the source value (like Excel).
- Requires
editable={true}andcellSelection={true}(default). - Calls
valueParserper cell -- returnundefinedto skip. - Each filled cell fires
onCellValueChanged. - Uses
requestAnimationFramefor smooth drag performance.
Undo / Redo
The useUndoRedo hook tracks edit history (including paste, fill, and delete). See the quick example above.
| Hook Input | Type | Description |
|---|---|---|
data | T[] | Current data array |
setData | (data: T[]) => void | State setter |
getRowId | (item: T) => RowId | Row identifier |
| Hook Output | Type | Description |
|---|---|---|
handleCellValueChanged | (event) => void | Wraps edits onto the undo stack |
undo / redo | () => void | Revert / re-apply |
canUndo / canRedo | boolean | Stack status |
Shortcuts: Ctrl+Z (undo), Ctrl+Y (redo). Also available in the right-click context menu.
Props Summary
| Prop | Type | Description |
|---|---|---|
editable | boolean | Enable editing grid-wide (on OGrid) |
editable | boolean | (item: T) => boolean | Per-column/row (on IColumnDef) |
cellEditor | 'text' | 'select' | 'checkbox' | 'richSelect' | ComponentType | Editor type |
cellEditorPopup | boolean | Render custom editor in a popover |
cellEditorParams | CellEditorParams | Editor parameters (e.g., { values: [...] }) |
valueParser | (params) => unknown | Validate on edit/paste/fill/delete. Return undefined to reject. |
valueFormatter | (value, item) => string | Format display & copy output |
onCellValueChanged | (event) => void | Fired after any cell value change |
onUndo / onRedo | () => void | Undo/redo callbacks |
canUndo / canRedo | boolean | Enable/disable undo/redo in context menu |
Related
- Spreadsheet Selection -- select cells for editing
- Context Menu -- right-click menu with undo/redo
- Custom Cell Editors -- advanced editor guide