Saving values
When the user commits an edit, the new value is written into the grid's
internal data copy. The grid does not mutate the array you passed
in via the data prop - it keeps its own working copy so an undo / cancel
is possible without touching your state.
To round-trip edits back to your source there are two patterns.
Live demo - typed editors, dirty tracking, save button:
Open the live example: Inline editing (Editing)
Pattern A - onCellValueChange (recommended)
The wrapper fires onCellValueChange whenever an inline edit commits.
The payload contains everything you need to forward the edit to a server,
update a cached aggregate, or push to an undo stack:
<script lang="ts">
type Event = {
rowIndex: number
columnId: string
oldValue: unknown
newValue: unknown
row: Person
}
function onCellValueChange(event: Event) {
// 1. ship the change
savePersonField(event.row.id, event.columnId, event.newValue)
// 2. update derived data (totals, dependencies, etc.)
// see the cascade-editing demo for the full pattern
}
</script>
<SvGrid
{data}
{columns}
features={features}
enableInlineEditing={true}
onCellValueChange={onCellValueChange}
/>
The wrapper has already written the parsed value into the row by the
time the callback fires, so event.row reflects the post-edit state.
The 18-cascade-editing demo wires this
into a recompute pipeline.
Pattern B - read a snapshot from the API
Useful for batch saves, "click Save to commit" UIs, or diffing against a before-snapshot:
<script lang="ts">
let api = $state<SvGridApi<typeof features, Person> | null>(null)
function captureChanges() {
if (!api) return
const after = api.getData()
// diff against your before-snapshot, send to server, etc.
}
</script>
<SvGrid {data} {columns} features={features}
enableInlineEditing
onApiReady={(next) => (api = next)} />
<button onclick={captureChanges}>Save</button>
The 05-inline-editing demo shows
this pattern with dirty-cell tracking against an initial snapshot.
Sync data both ways
Replacing the data prop forces the grid to re-read it. To mirror the
grid's internal copy back into a parent $state so other UI can react,
use onCellValueChange:
<script lang="ts">
let rows = $state<Person[]>([...])
function onCellValueChange(e: { rowIndex: number; row: Person }) {
rows = rows.map((r, i) => (i === e.rowIndex ? e.row : r))
}
</script>
<SvGrid data={rows} {columns} features={features}
enableInlineEditing
onCellValueChange={onCellValueChange} />
Persisting one cell
The event tells you exactly what changed - which row, which column, from what to what. That is enough for a PATCH, and it is why you rarely need to diff the whole row.
<script lang="ts">
import { SvGrid, type GridColumns, type SvGridApi } from '@svgrid/grid'
type Person = {
id: number
name: string
email: string
city: string
age: number
salary: number
}
const seed: Person[] = [
{ id: 1, name: 'Ada Lovelace', email: '[email protected]', city: 'London', age: 36, salary: 142000 },
{ id: 2, name: 'Grace Hopper', email: '[email protected]', city: 'New York', age: 45, salary: 168000 },
{ id: 3, name: 'Linus Torvalds', email: '[email protected]', city: 'Portland', age: 54, salary: 155000 },
]
let rows = $state<Person[]>(seed.map((p) => ({ ...p })))
let log = $state<string[]>([])
// What you would send. A real one awaits fetch and reverts on failure.
function save(field: string, id: number, value: unknown) {
log = ['PATCH /people/' + id + ' { ' + field + ': ' + JSON.stringify(value) + ' }', ...log].slice(0, 4)
}
const columns: GridColumns<Person> = [
{ field: 'name', header: 'Name', width: 180, editorType: 'text' },
{ field: 'city', header: 'City', width: 150, editorType: 'text' },
{ field: 'age', header: 'Age', width: 90, editorType: 'number' },
]
</script>
<SvGrid
data={rows}
{columns}
getRowId={(r) => String(r.id)}
editable
onCellValueChange={(e) => save(e.columnId, e.row.id, e.newValue)}
/>
<ul>
{#each log as line}<li><code>{line}</code></li>{/each}
</ul>
Rolling back a failed save
A save that fails has to put the value back, or the screen quietly disagrees with the server. Holding the old value across the await is the whole trick.
<script lang="ts">
import { SvGrid, type GridColumns, type SvGridApi } from '@svgrid/grid'
type Person = {
id: number
name: string
email: string
city: string
age: number
salary: number
}
const seed: Person[] = [
{ id: 1, name: 'Ada Lovelace', email: '[email protected]', city: 'London', age: 36, salary: 142000 },
{ id: 2, name: 'Grace Hopper', email: '[email protected]', city: 'New York', age: 45, salary: 168000 },
{ id: 3, name: 'Linus Torvalds', email: '[email protected]', city: 'Portland', age: 54, salary: 155000 },
]
let rows = $state<Person[]>(seed.map((p) => ({ ...p })))
let status = $state('idle')
// Fails on purpose for anything under 18, to show the rollback path.
async function persist(row: Person, field: string, value: unknown, previous: unknown) {
status = 'saving'
await new Promise((r) => setTimeout(r, 500))
if (field === 'age' && Number(value) < 18) {
(row as Record<string, unknown>)[field] = previous
status = 'rejected by server, rolled back'
return
}
status = 'saved'
}
const columns: GridColumns<Person> = [
{ field: 'name', header: 'Name', width: 180, editorType: 'text' },
{ field: 'age', header: 'Age', width: 100, editorType: 'number' },
]
</script>
<SvGrid
data={rows}
{columns}
editable
onCellValueChange={(e) => persist(e.row, e.columnId, e.newValue, e.oldValue)}
/>
<p>Status: <strong>{status}</strong> (try an age under 18)</p>
See also
Live examples
- Inline editing - Typed editors (text/number/checkbox/date) with dirty tracking + save.
- Cascade editing - Spreadsheet invoice: editing qty / price / discount cascades into line totals.
Related articles
- SvGrid Tips and Tricks: Get More from Your Svelte Data Grid - Practical SvGrid tips - fitColumns, cellFlash, Kanban board mode, server-side data, theming tokens and headless rendering - each with a code snippet and docs link.