Undo / redo

There is no built-in undo/redo stack inside <SvGrid>. The grid emits no per-edit event for an external stack to subscribe to, so undo/redo has to be built on top of the data-snapshot approach.

Open the live example: State maintenance (Rows & Cells)

Pattern - snapshot stack

The examples on this page run against these rows:

<script lang="ts">
  import { SvGrid, type GridColumns, type SvGridApi } from '@svgrid/grid'

  type Person = {
    id: number
    name: string
    department: string
    city: string
    age: number
    salary: number
  }

  const people: Person[] = [
    { id: 1, name: 'Ada Lovelace',   department: 'Engineering', city: 'London',   age: 36, salary: 142000 },
    { id: 2, name: 'Grace Hopper',   department: 'Engineering', city: 'New York', age: 45, salary: 168000 },
    { id: 3, name: 'Linus Torvalds', department: 'Platform',    city: 'Portland', age: 54, salary: 155000 },
    { id: 4, name: 'Radia Perlman',  department: 'Networking',  city: 'Seattle',  age: 49, salary: 161000 },
    { id: 5, name: 'Barbara Liskov', department: 'Platform',    city: 'Boston',   age: 52, salary: 172000 },
  ]

  const columns: GridColumns<Person> = [
    { field: 'name',       header: 'Name',       width: 190, editorType: 'text' },
    { field: 'department', header: 'Department', width: 150, editorType: 'text' },
    { field: 'city',       header: 'City',       width: 130, editorType: 'text' },
    { field: 'age',        header: 'Age',        width: 80,  editorType: 'number' },
    { field: 'salary',     header: 'Salary',     width: 130, editorType: 'number', format: { type: 'currency', currency: 'USD' } },
  ]
</script>
<script lang="ts">
  type Snapshot = Person[]

  let rows = $state<Person[]>(initial)
  let past = $state<Snapshot[]>([])
  let future = $state<Snapshot[]>([])
  const LIMIT = 50

  // Whenever rows change because of an edit, push the previous state onto `past`.
  let last = JSON.stringify(rows)
  $effect(() => {
    const next = JSON.stringify(rows)
    if (next === last) return
    past = [...past.slice(-LIMIT), JSON.parse(last)]
    future = []
    last = next
  })

  function undo() {
    const prev = past.at(-1)
    if (!prev) return
    future = [rows.map((r) => ({ ...r })), ...future].slice(0, LIMIT)
    rows = prev.map((r) => ({ ...r }))
    past = past.slice(0, -1)
    last = JSON.stringify(rows)
  }

  function redo() {
    const next = future[0]
    if (!next) return
    past = [...past, rows.map((r) => ({ ...r }))].slice(-LIMIT)
    rows = next.map((r) => ({ ...r }))
    future = future.slice(1)
    last = JSON.stringify(rows)
  }
</script>

<svelte:window onkeydown={(e) => {
  const meta = e.ctrlKey || e.metaKey
  if (meta && e.key === 'z' && !e.shiftKey) { e.preventDefault(); undo() }
  if (meta && ((e.key === 'z' && e.shiftKey) || e.key === 'y')) { e.preventDefault(); redo() }
}} />

<SvGrid data={rows} {columns} features={features} enableInlineEditing />

Why JSON snapshots

Without a per-edit event from the grid, the safest store of "previous state" is a deep copy of the row array. For a 5,000-row grid the snapshot is a few hundred KB - fine. For a 100,000-row grid, switch to a row-diff stack:

type Diff = { rowId: string; column: string; before: unknown; after: unknown }

…and apply it during undo/redo. The per-edit event needed to compute that cleanly is on the gap list.

Tracked at

Missing features - first-class undo stack with onCellValueChange to drive it.

More examples

Undo / redo (Ctrl+Z)

api.undo() / api.redo() + Ctrl+Z / Ctrl+Y / Ctrl+Shift+Z. 200-step bounded history; clearHistory after a successful save resets the baseline.

Open the live example: Undo / redo (Ctrl+Z) (Editing)

Try it

Edit a few cells, then step backwards. canUndo and canRedo are what the buttons should read - a disabled button is better than one that silently does nothing.

<script lang="ts">
  let api = $state<SvGridApi<{}, Person> | null>(null)
  let version = $state(0)

  // The api's undo state is not reactive on its own; bumping a counter after
  // every edit is what re-reads canUndo/canRedo for the button labels.
  const touch = () => (version += 1)
</script>

<div>
  <button type="button" disabled={!(version >= 0 && api?.canUndo())} onclick={() => { api?.undo(); touch() }}>
    Undo
  </button>
  <button type="button" disabled={!(version >= 0 && api?.canRedo())} onclick={() => { api?.redo(); touch() }}>
    Redo
  </button>
</div>

<SvGrid
  data={people}
  {columns}
  editable
  onCellValueChange={touch}
  onApiReady={(next) => (api = next)}
/>

See also

Live examples

  • State maintenance - Capture / restore the grid's sort, filters, visibility, widths, page, selection. Undo / redo history, named bookmarks, JSON import/export, debounced localStorage auto-save.
  • Undo / redo (Ctrl+Z) - `api.undo()` / `api.redo()` + Ctrl+Z / Ctrl+Y / Ctrl+Shift+Z. 200-step bounded history; clearHistory after a successful save resets the baseline.
  • Inline editing - Typed editors (text/number/checkbox/date) with dirty tracking + save.