# Editing

> Cell editing, validation, applyEdit, history and undo as an opt-in module, and the keyboard flow in an editing cell.

Page: https://svelte-lean.vercel.app/table/editing · Markdown: https://svelte-lean.vercel.app/table/editing.md · Index: https://svelte-lean.vercel.app/llms.txt

The renderer opens an editor, parses and validates the text, and reports one typed event. Committing the value, persisting it and undoing it are application code, with helpers in a separate entry point.

- **Source**: editing.tsdata-table.svelte
- **Tests**: core.test.tsdata-table.dom.test.tstable.test.tstable.spec.ts (browser)
- **Fixture**: fixtures/table-basic
- **Benchmark**: table.bench.spec.ts

Setting `onEdit` on the table and `editable` on a column is all the renderer needs. The editor is a native input or select chosen by `editor`; it is not a nested form, so a table inside a form never submits it. Enter saves, Escape cancels, and focus returns to the cell in every case. The table never mutates the row: the event carries the previous and the new value, and the application decides what to do with them.

`@svelte-lean/table/editing` holds the pure helpers: `applyEdit()` returns a new array with an immutable copy of the edited row, `setValue()` writes a dot path while refusing prototype-mutating segments, and `createHistory()` is a bounded undo stack the caller owns. The entry point measures 707 B gzip and a build without editing helpers renders only `parseEdit` from it, asserted by the `table-basic` fixture.

`the edit flow`

```ts
// 1. The renderer parses the editor text (parseEdit or the column's parse)
// 2. and validates it (the column's validate); an error stays in the editor.
// 3. onEdit receives { row, rowId, columnId, previous, value } and may return a promise.
// 4. The application commits: applyEdit() returns a new array with one new row object.
async function edit(event: EditEvent<Person>) {
	await api.patch(event.rowId, { [event.columnId]: event.value }); // the editor waits
	rows = applyEdit(rows, event, columns, (row) => row.id);
}
```

## Cell editing and history

Validate cell edits, persist them in the application and add undo only when needed.

Editing.svelte

```svelte
<script lang="ts">
	import { DataTable, type Column, type EditEvent } from '@svelte-lean/table';
	import { applyEdit, createHistory } from '@svelte-lean/table/editing';
	import { money, people, type Person } from '$lib/demo/data';

	let rows = $state(people(6));
	const history = createHistory(rows); // caller-owned undo, not part of the renderer
	let tick = $state(0);
	const columns: Column<Person>[] = [
		{ id: 'name', header: 'Member', editable: true },
		{
			id: 'status',
			header: 'Status',
			editable: true,
			editor: 'select',
			options: ['Active', 'Away', 'Invited'].map((value) => ({ label: value, value }))
		},
		{
			id: 'revenue',
			header: 'Revenue',
			align: 'end',
			editable: (row) => row.status !== 'Invited',
			editor: 'number',
			validate: (value) => (Number(value) < 0 ? 'Revenue cannot be negative.' : undefined),
			format: (value) => money.format(Number(value))
		},
		{ id: 'joined', header: 'Joined', editable: true, editor: 'date' }
	];

	// The table hands over a parsed, validated event; the application commits an immutable copy.
	function edit(event: EditEvent<Person>) {
		rows = history.commit(applyEdit(rows, event, columns, (row) => row.id));
		tick++;
	}
</script>

<button type="button" data-slean="button" data-variant="outlined" data-color="neutral" disabled={tick < 0 || !history.canUndo}
	onclick={() => { rows = history.undo(); tick++; }}>Undo</button>
<button type="button" data-slean="button" data-variant="outlined" data-color="neutral" disabled={tick < 0 || !history.canRedo}
	onclick={() => { rows = history.redo(); tick++; }}>Redo</button>

<DataTable data={rows} {columns} getRowId={(row) => row.id} onEdit={edit} pagination={false} label="Members" />
```

Member is a text editor, Status a select, Revenue a number editor that rejects negative values and is read-only for invited members, Joined a date editor. Double-click a cell, press Enter or F2 on it, or use its edit button. Undo and Redo are Button primitives over createHistory().

- A rejected value shows the message from `validate` inside the editor and keeps it open.
- While `onEdit` is pending the editor is disabled; an error it throws is shown the same way.
- Columns with a function accessor cannot be written by `applyEdit()`; use an application setter.

## Keyboard

With `onEdit` set, the body follows the grid pattern: one cell carries `tabindex="0"`, the others `-1`, and the per-cell edit buttons are not in the Tab order. The full grid keyboard model is on the [Data Grid](https://svelte-lean.vercel.app/table/data-grid.md) page.

| Key | When | Result |
| --- | --- | --- |
| `Tab` | outside the body | Enters the body on the last cell reached, or the first cell. |
| `Enter`/`F2` | on an editable cell | Opens the editor with the current value and focuses it. |
| `Enter` | in the editor | Parses, validates and saves; focus returns to the cell. |
| `Escape` | in the editor | Cancels; focus returns to the cell. |
| `ArrowLeft`/`ArrowRight`/`ArrowUp`/`ArrowDown` | on a cell | Moves to the neighbouring cell by structure. |

## API

| Name | Of | Type | Default | Description |
| --- | --- | --- | --- | --- |
| [`onEdit`](https://svelte-lean.vercel.app/table/api.md#prop-onedit) | Events and composition | `(event: EditEvent<T>) => void \| Promise<void>` | `undefined` | Receives a parsed and validated cell edit; enables editing and cell focus. The table never mutates rows. |
| [`editable`](https://svelte-lean.vercel.app/table/api.md#column-editable) | Editing | `boolean \| ((row: T) => boolean)` | `false` | Cell editing for the column or per row; needs onEdit on the table. |
| [`editor`](https://svelte-lean.vercel.app/table/api.md#column-editor) | Editing | `'text' \| 'number' \| 'date' \| 'select'` | `'text'` | Native input type of the editor. |
| [`options`](https://svelte-lean.vercel.app/table/api.md#column-options) | Editing | `{ label: string; value: string }[]` | `[]` | Options of the select editor. |
| [`parse`](https://svelte-lean.vercel.app/table/api.md#column-parse) | Editing | `(value: string, row: T) => unknown` | `parseEdit` | Converts the editor text to the domain value. |
| [`validate`](https://svelte-lean.vercel.app/table/api.md#column-validate) | Editing | `(value: unknown, row: T) => string \| undefined` | `undefined` | Returns an error message that blocks onEdit and is shown in the editor. |
| [`edit`](https://svelte-lean.vercel.app/table/api.md#message-edit) | TableMessages | `string` | `'Edit'` | Prefix of the per-cell edit button label. |
| [`save`](https://svelte-lean.vercel.app/table/api.md#message-save) | TableMessages | `string` | `'Save'` | Editor button. |
| [`cancel`](https://svelte-lean.vercel.app/table/api.md#message-cancel) | TableMessages | `string` | `'Cancel'` | Editor button. |
| [`EditEvent<T>`](https://svelte-lean.vercel.app/table/api.md#type-editevent-t-) | Events and positions | `{ row: T; rowId: RowId; columnId: string; previous: unknown; value: unknown }` | – | The parsed edit passed to onEdit and produced by pasteRange(). |
| [`@svelte-lean/table/editing`](https://svelte-lean.vercel.app/table/api.md#module--svelte-lean-table-editing) | Entry points | `applyEdit · setValue · parseEdit · createHistory · editEvent` | – | Immutable edit application and a caller-owned undo history. |

## Related

- [Data Grid](https://svelte-lean.vercel.app/table/data-grid.md)
- [Clipboard & Export](https://svelte-lean.vercel.app/table/clipboard-export.md)
- [Server data](https://svelte-lean.vercel.app/table/server-data.md)
