Skip to main content

Editing & Clipboard

Double-click a cell to edit it. Hit Enter to save, Escape to cancel. It feels like a spreadsheet because it's built to work like one — with clipboard paste, drag-to-fill, and full undo/redo out of the box.

Double-click any cell to edit, or press F2
Live
Try it in your framework

The demo above uses Radix UI for styling. Each framework renders editors with its native components — dropdowns, date pickers, and inputs that match your design system.

Get started in 5 minutes​

Here's a realistic setup: an employee table with text, dropdown, and numeric columns — including validation that rejects bad salary values before they're saved.

import { OGrid, useUndoRedo } from '@alaarab/ogrid-react-radix';
import { useCallback, 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; // undefined = reject
},
valueFormatter: (v) => `$${Number(v).toLocaleString()}`,
},
];

function App() {
const [data, setData] = useState(initialData);
const applyEdit = useCallback((event) => {
setData((prev) => prev.map((row) => (row.id === event.item.id ? { ...row, [event.columnId]: event.newValue } : row)));
}, []);
const { onCellValueChanged, undo, redo, canUndo, canRedo } = useUndoRedo({ onCellValueChanged: applyEdit });

return (
<OGrid
columns={columns}
data={data}
getRowId={(item) => item.id}
editable
onCellValueChanged={onCellValueChanged}
onUndo={undo}
onRedo={redo}
canUndo={canUndo}
canRedo={canRedo}
/>
);
}
Switching UI libraries

Same props across all React packages — 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>

That single setup gives you inline editing, clipboard paste, fill handle, and full undo/redo.

Editing a cell​

Double-click or press F2 to open the editor. When you're done:

  • Enter — saves and moves focus down
  • Tab — saves and moves focus right
  • Escape — cancels, restores the original value

Which editor should I use?​

cellEditorWhen to use it
'text' (default)Freeform text — names, notes, anything open-ended
'select'Small, fixed list where the user picks exactly one value
'richSelect'Longer list with keyboard search — much nicer UX than a plain dropdown
'checkbox'Boolean toggle — renders as a checkbox, commits on click

Searchable dropdown (richSelect)​

When your list gets longer than about 8 items, switch to richSelect. It adds a search box and full keyboard navigation:

{
columnId: 'category',
editable: true,
cellEditor: 'richSelect',
cellEditorParams: {
values: ['Engineering', 'Design', 'Marketing', 'Sales', 'Legal', 'Finance'],
formatValue: (v) => `Dept: ${v}`, // optional display formatting
},
}

Lock specific rows from editing​

Use a function for editable to make it conditional:

{ columnId: 'name', editable: (item) => item.status !== 'locked' }

The cell still renders normally — it just won't open an editor when you click it.

Custom editor (popup)​

For date pickers, color pickers, or any custom UI, pass your own component. Set cellEditorPopup: true to render it in a floating popover instead of inline:

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 }

Validating input with valueParser​

valueParser runs on every edit, paste, fill, and delete. Return undefined to silently reject the value — the cell snaps back to its previous state:

{
columnId: 'age',
editable: true,
type: 'numeric',
valueParser: ({ newValue }) => {
const num = Number(newValue);
return isNaN(num) || num < 0 || num > 150 ? undefined : num;
},
}
Pro tip

valueParser is your single source of truth for validation — it runs on inline edits, clipboard paste, fill handle, and delete. You don't need separate logic for each.

Clipboard​

Select cells, then Ctrl+C to copy, Ctrl+V to paste
Live

Standard shortcuts work on any selection — no configuration needed:

KeyActionRequires editable
Ctrl+C / Cmd+CCopy as tab-separated textNo
Ctrl+V / Cmd+VPaste into cellsYes
Ctrl+X / Cmd+XCut (copy then clear)Yes
Delete / BackspaceClear selected cellsYes

Copy respects valueFormatter — what you see is what you get in the clipboard. Paste calls valueParser on each value, so bad data is rejected before it lands in your rows.

The keyboard shortcuts go through the browser's native clipboard events: Ctrl+C and Ctrl+X put the TSV on the copy / cut event's clipboardData, and Ctrl+V and Shift+Insert paste from the paste event. That works on plain http, where navigator.clipboard does not exist, and in Firefox and Safari, where navigator.clipboard.readText is denied by default. Only the context menu's Copy, Cut and Paste use the clipboard programmatically (writeText / readText); a rejected read there calls onClipboardError and pastes nothing.

Fill handle​

Select a cell, then drag the green square at the bottom-right corner to fill
Live

The small square in the bottom-right corner of a selection — drag it down (or up, or sideways) to fill cells. Works exactly like Excel: the fill extends the selection along one axis at a time, whichever the pointer is farther outside the selection on, so a diagonal drag fills down or across but never both. Dragging back inside the selection fills nothing. Each filled cell goes through valueParser, so validation still applies.

Pro tip

Select a range of cells before dragging the fill handle to populate multiple columns at once. The fill handle uses requestAnimationFrame internally, so it stays smooth even on large selections.

Undo / Redo​

The useUndoRedo hook (React) tracks everything: inline edits, paste, fill, and delete. Ctrl+Z / Ctrl+Y work out of the box, and you can wire up buttons too:

const applyEdit = useCallback((event) => {
setData((prev) => prev.map((row) => (row.id === event.item.id ? { ...row, [event.columnId]: event.newValue } : row)));
}, []);
const { onCellValueChanged, undo, redo, canUndo, canRedo } = useUndoRedo({ onCellValueChanged: applyEdit });

Pass the returned onCellValueChanged to <OGrid> instead of your raw handler — it records each change on the undo stack before calling applyEdit. See useUndoRedo for batching, clear(), and formula integration. Undo/redo are also available in the right-click context menu.

With formulas on, a host-owned history (onUndo / onRedo) receives formula edits as value changes whose value is the formula text; store it in your data and undo works for formulas too. See Formulas: undo with host-controlled history.

Props reference​

PropTypeDescription
editablebooleanEnable editing grid-wide (on OGrid)
editableboolean | (item: T) => booleanPer-column/row editability (on IColumnDef)
cellEditor'text' | 'select' | 'checkbox' | 'richSelect' | ComponentTypeEditor to use
cellEditorPopupbooleanRender editor in a floating popover
cellEditorParamsCellEditorParamsEditor parameters (e.g., { values: [...] })
valueParser(params) => unknownValidate/transform on edit, paste, fill, delete. Return undefined to reject.
valueFormatter(value, item) => stringFormat value for display and clipboard copy
onCellValueChanged(event) => voidFires after any cell value change
onUndo / onRedo() => voidUndo/redo callbacks. When set, they replace the grid's internal history (with formulas, see host-controlled history).
canUndo / canRedobooleanEnable/disable undo/redo in context menu

Headless alternative​

Own your table markup? The same behavior is available as hooks: useInlineEdit (editing), useCellClipboard (copy / cut / paste), useFillHandle (drag-to-fill), and useUndoRedo. Start at the Headless overview.

Next steps​