Cell Editing
Edit cell values inline with validation, formatting, and multiple input types.
How to Enable
- Set
enableCellEditing: trueon the table options - Set
editable: trueon columns that should be editable - Provide an
editConfigwith the editor type - Handle commits via
onEditCommit
Edit Config
interface CellEditConfig {
type: 'text' | 'number' | 'select' | 'toggle' | 'date' | 'checkbox' | 'custom'
options?: { label: string; value: unknown }[] // For 'select' type
getOptions?: (row: Row) => { label: string; value: unknown }[] // Dynamic options
validate?: (value, row) => string | null // Return error message or null
parse?: (inputValue: string) => TValue // Parse input string to typed value
format?: (value: TValue) => string // Format value for display
placeholder?: string
render?: (props: CellEditRenderProps) => unknown // Custom editor render
commit?: (row: Row<TData>, value: TValue) => void // Per-column commit handler
}
Example
const columns = [
columnHelper.accessor('name', {
header: 'Name',
editable: true,
editConfig: {
type: 'text',
placeholder: 'Enter name...',
validate: (value) => {
if (!value) return 'Required'
if (String(value).length < 2) return 'Too short'
return null
},
},
}),
columnHelper.accessor('status', {
header: 'Status',
editable: true,
editConfig: {
type: 'select',
options: [
{ label: 'Active', value: 'active' },
{ label: 'Inactive', value: 'inactive' },
{ label: 'Pending', value: 'pending' },
],
},
}),
columnHelper.accessor('approved', {
header: 'Approved',
editable: true,
editConfig: { type: 'toggle' },
}),
]
const table = useTable({
data,
columns,
enableCellEditing: true,
onEditCommit: (changes) => {
// changes: Record<rowId, Partial<TData>>
console.log('Committed:', changes)
},
})
Commit payloads are keyed by column id
The onEditCommit payload is Record<rowId, Record<columnId, value>> — keyed by
column id, not data-field path. For a string-path column the id defaults to
the path, so the patch looks like a Partial<TData>. But a function / derived
accessor sets its own id, and the committed value lands under that id, not under
any real field:
columnHelper.accessor((row) => row.pricing.tier1, {
id: 'tier1Price', // commit key is 'tier1Price', not 'pricing.tier1'
editable: true,
editConfig: { type: 'number' },
})
// onEditCommit receives: { [rowId]: { tier1Price: 42 } }
// NOT { [rowId]: { pricing: { tier1: 42 } } }
Per-column commit handler
editConfig.commit(row, value) fires once for every committed value in that
column — on single-cell commit, full-row commit, and commitAllPending() — with
the pre-commit row and the new value. It lets the column-id → data-field mapping
live on the column def instead of a switch (columnId) inside onEditCommit:
columnHelper.accessor((row) => row.pricing.tier1, {
id: 'tier1Price',
editable: true,
editConfig: {
type: 'number',
commit: (row, value) => updateTierPrice(row.original.id, 'tier1', value),
},
})
commit fires regardless of whether onEditCommit / onCommit is also set — if
both are defined, both run, so pick one owner per column.
Editors render automatically
An editConfig renders the matching built-in editor as soon as a cell enters
edit mode — you do not need to hand-wire <CellInput> in a cell renderer. The
type maps to: text/number/email/url/tel → text input, select →
dropdown, toggle/checkbox → checkbox, date → date input, and custom
(with render) → your own editor.
A column may define both a custom cell renderer (styled display) and an
editConfig. The custom cell is used for display mode only; while editing,
the configured editor takes over automatically:
columnHelper.accessor('price', {
editable: true,
editConfig: { type: 'number' },
cell: (ctx) => <span className="tabular-nums">{fmt(ctx.getValue())}</span>,
})
For a fully custom editor use editConfig.render; to render your own editor
inside the cell, branch on ctx.cell.getIsEditing() and omit editConfig.
Table Methods
table.startEditing(rowId, columnId) // Enter edit mode on a cell
table.commitEdit() // Commit the active edit
table.cancelEdit() // Cancel the active edit
table.setPendingValue(rowId, columnId, value) // Set a pending value
table.getPendingValue(rowId, columnId) // Read a pending value
table.getAllPendingChanges() // Get all uncommitted changes
table.hasPendingChanges() // Check if any changes are pending
table.commitAllPending() // Commit all pending changes at once
table.discardAllPending() // Discard all pending changes
table.getValidationErrors() // Get validation errors
table.isValid() // Check if all pending values are valid
Full-Row Editing Controls
Use RowEditControls in a display column to switch a row between view mode and full-row edit mode. The control renders Edit, Save, and Cancel actions, disables Save while row validators fail, and works in both desktop table rows and adaptive card layouts.
import { CellInput, RowEditControls, createColumnHelper } from '@zvndev/yable-react'
const columnHelper = createColumnHelper<Employee>()
const columns = [
columnHelper.accessor('name', {
header: 'Name',
editable: true,
editConfig: { type: 'text' },
cell: (context) =>
context.table.isRowEditing(context.row.id) ? (
<CellInput context={context} />
) : (
context.getValue()
),
}),
columnHelper.display({
id: 'actions',
header: 'Actions',
cell: (context) => <RowEditControls context={context} />,
}),
]
When a row is editing, bundled form controls defer blur, Enter, Escape, and Tab behavior to the row edit session: Enter saves the row, Escape cancels, and Tab cycles through editable cells in that row.
Always-Editable Cells
To make a cell always show its editor (like a spreadsheet), set alwaysEditable in the column meta:
columnHelper.accessor('quantity', {
header: 'Qty',
editable: true,
editConfig: { type: 'number' },
meta: { alwaysEditable: true },
})