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.