Appearance
Editing
By default every cell is read-only. Mark a <Column editable> (and bind r-model:data to receive the writes) to make that column's cells editable — the component owns the edit state: the consumer binds the single data model and listens for the commit events, with no manual re-sync. A committed edit writes a fresh data array back (never an in-place mutation).
Editing requires grid mode
Every pointer and keyboard edit-entry path — double-click, singleClickEdit, a printable keypress, F2, and Shift+F2 for full-row edit — is gated on interactionMode="grid". In the default interactionMode="table" a <Column editable> column renders read-only and the only way into an editor is the imperative editCell / editRow handle verbs. Set interactionMode="grid" on the table whenever you want editing.
rozie
<DataTable :data="$data.rows" r-model:data="$data.rows" interactionMode="grid"
@cell-edit-commit="onCommit($event)">
<Column field="name" header="Name" editable editor="text" />
<Column field="qty" header="Qty" editable editor="number" :validate="(v) => Number(v) >= 0 || 'must be >= 0'" />
<Column field="status" header="Status" editable editor="select" :editorOptions="$data.statusOptions" />
<Column field="active" header="Active" editable editor="checkbox" />
</DataTable>A cell enters edit mode on double-click, on F2 / Enter (which seeds the current value), or via the editCell(rowIndex, colIndex) verb. Set singleClickEdit to open the editor on a single click instead of a double-click. Enter commits; Escape cancels.
A printable keypress also opens the editor, but what it does with the typed character depends on the editor type: on text / number it seeds the editor with that character (replacing the current value); on select / custom it opens with the current value instead, exactly like F2 / Enter (a typed character is not a valid option or custom value, so it is not used to seed). A checkbox cell is different again — Space, Enter, and F2 toggle and commit it instantly, with no editor opening at all; no other printable key does anything on a checkbox cell.
Built-in editor types (the editor Column prop): 'text' (default <input type="text">), 'number', 'select' (populate editorOptions with [{ value, label }]), 'checkbox', and 'custom' (no built-in editor — the #editor slot drives it).
Validation. The validate Column prop runs synchronously on commit: return true/falsy to accept, or a string to reject — the editor stays open and the message is announced via an aria-live region, and cell-edit-commit does not fire. Validators are defensively wrapped (a thrown error coerces to a generic message).
Full-row edit. Shift+F2 on a row (or the editRow(rowIndex) verb) opens every editable cell in the row at once; Tab/Shift+Tab move between editors. Enter commits the whole row in one r-model:data write + one row-edit-commit (validated together — one failure blocks the commit); Escape reverts the row as a unit. Like every other pointer/keyboard edit-entry path, Shift+F2 is gated on interactionMode="grid"; the imperative editRow verb is not.
The #editor scoped slot (scope { columnId, column, row, value, commit, cancel, autofocus }) replaces the built-in editor for a column — dispatch by columnId. commit(newValue) validates + commits (firing cell-edit-commit); cancel() closes without saving; autofocus tells the editor whether the host wants it to take initial focus — drive your input's focus from it.
The grid keeps its keymap around your editor: Tab/Shift+Tab commit and move to the next/previous editable cell, exactly as they do for the built-in editors, and in full-row edit Enter commits the whole row and Escape reverts it. To carry your editor's value, Tab moves focus off it, so call commit(draft) from your control's blur handler — every shipped drop-in does. If your editor handles Tab itself (a multi-field editor, say), call preventDefault() on the event and the grid leaves it alone. In full-row edit, commit(value) only stages that cell's draft; the row is written once, on Enter. On React/Solid it is the renderEditor / editorSlot render prop and on Lit the .editor property (the documented divergence).
Per-column editor-<columnId> slot family. Instead of dispatching #editor by columnId yourself, you can fill a single column's editor directly — #editor-status="{ row, value, commit, cancel }" — which wins over a generic #editor fill for that one column, which in turn wins over the built-in editor. Both the family and the generic #editor tier are gated identically: they reach only columns declaring editor="custom". See Columns — per-column slot families for the full precedence and gate rules shared across all four seams.
The commit events are cell-edit-commit (payload { rowId, columnId, oldValue, newValue }) and row-edit-commit (payload { rowId, changes } for the columns whose value actually changed) — see the Events reference. Drive editing imperatively with editCell / editRow / commitEditing.
Group-header rows are never editable. When grouping is active, a group-header row stands for a set of records rather than being one, so it has nothing to write to. No edit-entry path opens an editor on one — Enter, F2, Space, a printable key, click-to-edit and Shift+F2 all decline, and Enter toggles the group instead. Paste, cut, clear and fill-drag skip any group-header row inside the target range (a skipped cell still counts toward the "N of M cells" announcement). Copy is unaffected. This holds regardless of the column's own editable flag.
Changed in 0.3.2
Before 0.3.2 an editable non-grouping column would open an editor on a group-header row, and committing wrote to the group's first member record — silently modifying a row the user was not editing. Paste and fill-drag wrote through the same path. If your app allowed editing while grouped, audit for records changed this way.
What an emptied cell commits
Clearing a cell — deleting its text and committing, Delete/Backspace over a range, a Cut, or pasting an empty TSV field — does not commit the same value for every column. The commit funnel coerces by the column's editor type:
editor | An emptied cell commits |
|---|---|
number | null — there is no empty number, and Number('') is 0, which would silently write a real zero |
text (and the r-else fallback) | '' |
select | '' — a select's model type is the option string, so a string is already correct; an out-of-range option is a validation question, not a coercion one, and nothing is coerced here |
checkbox | false |
custom | whatever your #editor fill passes to commit(...); nothing is coerced |
A checkbox column coerces on the way in as well, because every path that writes without opening the built-in control — paste, Cut, Delete over a range, a fill-drag — arrives as a plain TSV string. true / 1 / yes / y / on (any case, any surrounding whitespace) and a non-zero number commit true; everything else, including '' and unrecognised text, commits false. That is lossy on purpose: the alternative is letting the literal text "maybe" land in a field your model declares boolean.
That is type-correct per column but it is not uniform across columns, which matters the moment one operation spans several: Delete over a range covering a text column and a number column writes '' into one and null into the other in the same r-model:data update. An "is this cell empty?" check on the consumer side has to accept both. The rule is stable and will not change silently — it is asserted per editor type in the grid-edit battery.
Drop-in editor components
The #editor slot is fully headless — you can render any control. For the common cases the package also ships opt-in drop-in editor components so you don't have to hand-roll the input wiring: EditorText, EditorNumber, EditorSelect, EditorCheckbox, and EditorDate. They are additive named exports alongside DataTable (which stays the headless default export — importing the editors is byte-identical-off if you never use them):
ts
import { DataTable, Column, EditorText, EditorNumber, EditorSelect, EditorCheckbox, EditorDate }
from '@rozie-ui/data-table-<target>';
// (Vue: `import DataTable, { Column, EditorText, … }` — DataTable is the default.
// Lit: the single side-effect import registers the <rozie-editor-*> custom elements.)Each drop-in takes the #editor slot scope as its props — { columnId, column, row, value, commit, cancel, autofocus } — and EditorSelect additionally takes options: [{ value, label }] (the same shape as <Column editorOptions>). Mark the column editor="custom" so the slot drives rendering, then dispatch by columnId inside #editor and forward the scope to the matching drop-in. Use them as-is, or fork one as a template for a bespoke editor.
| Component | Renders | Extra props |
|---|---|---|
EditorText | <input type="text"> | — |
EditorNumber | <input type="number"> | — |
EditorSelect | <select> | options: [{ value, label }] |
EditorCheckbox | <input type="checkbox"> | — |
EditorDate | <input type="date"> | — |
Per-framework code
The per-target wiring is the editable cells snippet and the drop-in editor components snippet on the usage page.
See also
- Columns — the
editable/editor/validateColumn attributes. - Grid mode & keyboard — pairs naturally with editing for spreadsheet-style entry.
- API reference — the
cell-edit-commit/row-edit-commitevents and the editing verbs.