developer guide
Data Grid Clipboard Copy and Paste with Excel
A range copies out in the layout Excel and Sheets expect, and a block pasted back is applied cell by cell through the same validation as typing. A preview shows what will change and what was refused before anything is written, and text that would act as a formula is neutralised as it arrives.
Developer guide › Export and clipboard › Data Grid Clipboard Copy and Paste with Excel
Clipboard
Copy produces the tab-separated form Excel, Numbers and Sheets all read, so a range pastes as cells rather than as one lump of text. Values go through each column's clipboard hook: a lookup copies its label, a date copies an unambiguous form.
Copy and paste
grid.export.rangeText(); // the text, without writing it
grid.export.clipboard({ rows: 'range' }); // copy the range
grid.export.clipboard({ headers: true, rows: 'selected' });
grid.edit.pasteInto(text); // Excel's tiling rules
Guarding against formula injection on paste
By default a copied range round-trips verbatim, so a value beginning =,
+, - or @ is carried unchanged - the right thing
when the paste target is another grid or cell range. When the paste target is a spreadsheet,
pass sanitise: true and a leading formula character is neutralised with an
apostrophe, so the value cannot be executed as a formula in Excel or Sheets.
Opt in per copy
const { buildClipboardText } = await import('../packages/core/src/export/index.js');
// An explicit cell matrix; the formula guard applies to data fields.
const cells = [['=1+1']];
// Off by default: the value round-trips exactly as copied.
const plain = buildClipboardText({}, { rows: 'range', cells });
// sanitise:true prefixes an apostrophe so a spreadsheet stores it as text.
const guarded = buildClipboardText({}, { rows: 'range', cells, sanitise: true });
return `default:${plain} guarded:${guarded}`;
Pasting follows the spreadsheet convention: one cell into a range fills the range, one row into several rows repeats down, and a block larger than the target extends past it. People have twenty years of muscle memory for this and a grid that invents its own rules is a grid people fight.
Previewing a bulk paste
A paste is the one clipboard gesture that can rewrite dozens of cells with nothing to inspect
first: a payload that lands a column to the left of where it was aimed, or over a range the
user forgot was selected, looks exactly like one that worked. Turn on
edit.pastePreview and a paste into more than one cell opens a confirm/cancel dialog
before anything commits.
Opt in
createGrid(host, {
// Off by default: an unconfigured grid pastes straight away, as before.
edit: { enabled: true, pastePreview: true },
});
The dialog lists every cell that will change, old → new, and every cell a commit
would reject - a read-only cell, a value the column's type or edit.validate
refuses, a cell a permission policy forbids. Confirm commits precisely that set
through the ordinary paste path; Cancel commits nothing. A single-cell paste
skips the dialog - a preview for one cell is friction, not a safety net. The dialog is a
modal role="dialog": Escape cancels, Tab stays inside it,
focus moves in on open and back on close, and its opening is announced through the grid's live
region. The same diff is available without any UI from
grid.edit.previewPaste(anchor, text, extent?), which returns
{ changes, rejected } and changes nothing.
Why a per-edit flag, off by default. Paste preview lives under
edit because a paste is a bulk edit and its accept/reject decisions are the edit
model's - the preview cannot disagree with the commit because it runs the same checks.
Off by default keeps every existing grid's paste behaviour exactly as it was.
Clearing cells with Delete
Delete or Backspace on a focused cell that is not being edited clears it;
with a cell range selected it clears every editable cell of the range as one undo
step. It is a user edit through the ordinary path, not a shortcut
around it: read-only columns and rows are skipped, beforeEdit can veto the clear,
a column's declarative validation judges the
empty value exactly as it judges a typed empty one (required refuses it with the
grid's own message; the shape rules - min, max,
pattern, oneOf - skip a blank, so they let it through), and change tracking, undo and write-back see each cell. The cleared value is
null - the empty value of every column type (a text column's own parse reads
the empty string as null as well). A screen reader hears “N cells
cleared”. The cell menu's Clear cells takes the same path.
Turn the key off
const { createHeadlessGrid } = await import('../packages/core/src/index.js');
const columns = [{ field: 'name', edit: true }];
const rows = [{ name: 'a' }];
// On by default. false leaves Delete and Backspace alone.
const on = createHeadlessGrid({ columns, rows, edit: { enabled: true } });
const off = createHeadlessGrid({ columns, rows, edit: { enabled: true, clearOnDelete: false } });
return `default ${on.edit.clearOnDelete} | off ${off.edit.clearOnDelete}`;
Undo and redo from the keyboard
Ctrl/Cmd+Z undoes and Ctrl+Y or
Ctrl/Cmd+Shift+Z redoes a step of the grid's history - the same timeline as grid.edit.undo(), so a typed edit, a paste, a fill and a
Delete clear are one step each. It works while focus is in the grid and no cell
editor is open: an open editor keeps the browser's own text undo. A screen reader hears what was
undone or redone. The keys do nothing when there is no history (undoDepth: 0, or nothing
recorded). edit.undoShortcuts: false leaves them alone.
Turn the keys off
const { createHeadlessGrid } = await import('../packages/core/src/index.js');
const columns = [{ field: 'name', edit: true }];
const rows = [{ name: 'a' }];
// On by default. false leaves Ctrl+Z / Ctrl+Y to the page.
const on = createHeadlessGrid({ columns, rows, edit: { enabled: true } });
const off = createHeadlessGrid({ columns, rows, edit: { enabled: true, undoShortcuts: false } });
return `default ${on.edit.undoShortcuts} | off ${off.edit.undoShortcuts}`;