Table Workflows
Editing
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
- Fixture
- Benchmark
On this page
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.
// 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.
Amara Okafor | Active | $4,501 | 2026-01-07 |
Jonas Lindqvist | Active | $12,420 | 2026-06-18 |
Mei Tanaka | Away | $20,339 | 2026-02-01 |
Rafael Duarte | Invited | $28,258 | 2026-07-12 |
Selin Aydın | Active | $36,177 | 2026-03-23 |
Noah Brenner | Active | $44,096 | 2026-08-06 |
<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="outline" disabled={tick < 0 || !history.canUndo}
onclick={() => { rows = history.undo(); tick++; }}>Undo</button>
<button type="button" data-slean="button" data-variant="outline" 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" />- A rejected value shows the message from
validateinside the editor and keeps it open. - While
onEditis 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 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 | 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 | Editing | boolean | ((row: T) => boolean) | false | Cell editing for the column or per row; needs onEdit on the table. |
editor | Editing | 'text' | 'number' | 'date' | 'select' | 'text' | Native input type of the editor. |
options | Editing | { label: string; value: string }[] | [] | Options of the select editor. |
parse | Editing | (value: string, row: T) => unknown | parseEdit | Converts the editor text to the domain value. |
validate | Editing | (value: unknown, row: T) => string | undefined | undefined | Returns an error message that blocks onEdit and is shown in the editor. |
edit | TableMessages | string | 'Edit' | Prefix of the per-cell edit button label. |
save | TableMessages | string | 'Save' | Editor button. |
cancel | TableMessages | string | 'Cancel' | Editor button. |
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 | Entry points | applyEdit · setValue · parseEdit · createHistory · editEvent | – | Immutable edit application and a caller-owned undo history. |