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.
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.
- 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; // undefined = reject
},
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}
/>
);
}
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> - 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. 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 API across Vue packages. 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,
});
grid.on('cellValueChanged', (event) => {
console.log(`${event.columnId} changed to ${event.newValue}`);
});
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?
cellEditor | When 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;
},
}
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
Standard shortcuts work on any selection — no configuration needed:
| Key | Action | Requires editable |
|---|---|---|
| Ctrl+C / Cmd+C | Copy as tab-separated text | No |
| Ctrl+V / Cmd+V | Paste into cells | Yes |
| Ctrl+X / Cmd+X | Cut (copy then clear) | Yes |
| Delete / Backspace | Clear selected cells | Yes |
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.
Fill handle
The small square in the bottom-right corner of a selection — drag it down to fill cells. Works exactly like Excel. Each filled cell goes through valueParser, so validation still applies.
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 { handleCellValueChanged, undo, redo, canUndo, canRedo } = useUndoRedo({
data,
setData,
getRowId: (item) => item.id,
});
Pass handleCellValueChanged as onCellValueChanged — it intercepts changes and pushes them onto the undo stack before updating your state. Undo/redo are also available in the right-click context menu.
Props reference
| Prop | Type | Description |
|---|---|---|
editable | boolean | Enable editing grid-wide (on OGrid) |
editable | boolean | (item: T) => boolean | Per-column/row editability (on IColumnDef) |
cellEditor | 'text' | 'select' | 'checkbox' | 'richSelect' | ComponentType | Editor to use |
cellEditorPopup | boolean | Render editor in a floating popover |
cellEditorParams | CellEditorParams | Editor parameters (e.g., { values: [...] }) |
valueParser | (params) => unknown | Validate/transform on edit, paste, fill, delete. Return undefined to reject. |
valueFormatter | (value, item) => string | Format value for display and clipboard copy |
onCellValueChanged | (event) => void | Fires after any cell value change |
onUndo / onRedo | () => void | Undo/redo callbacks |
canUndo / canRedo | boolean | Enable/disable undo/redo in context menu |
Next steps
- Spreadsheet Selection — select ranges before editing or pasting
- Context Menu — right-click to undo, copy, and more
- Formulas — formula-aware editing with
=SUM(A1:A5)syntax