Programmatic editing (start/stop)

Drive the editor from outside via api.startEditing(row, columnId) / stopEditing(cancel?). A toolbar edits the active cell, commits or cancels, and a guided-entry flow jumps to the next blank required field and opens it.

A live, editable Svelte 5 data grid example from the SvGrid gallery (Editing). See the SvGrid documentation for the full API.

About this example

Driving the Svelte 5 data grid editor from outside: api.startEditing(row, columnId) opens an editor, api.stopEditing(cancel) commits or cancels it. A toolbar edits the active cell, and a guided-entry flow uses api.getDisplayedRows() and api.selectCells() to jump to the next blank required field and open it, so a form-style entry experience is built entirely on the imperative API.

Drive the editor from outside the grid. A toolbar begins editing the active cell, commits or cancels it, and runs a "guided data entry" flow that jumps to the next blank required field and opens its editor - so a form-style entry experience is built entirely on the imperative API.

Imports, features and API used

Imports: @svgrid/grid

Table features registered: rowSortingFeature

Columns: company (Company), contact (Contact), email (Email), stage (Stage)

SvGridApi methods called: api.getDisplayedRows(), api.selectCells(), api.startEditing(), api.stopEditing()

Frequently asked questions

How do I open an editor from a button?

Track the active cell with onActiveCellChange, then call api.startEditing(rowIndex, columnId) from the button's handler. api.stopEditing() commits and api.stopEditing(true) cancels.

How does the guided-entry flow find the next blank field?

It scans api.getDisplayedRows() for the first required column that is empty, selects that cell with api.selectCells() and calls startEditing on it, so the user just types and presses Enter to move on.

How are cells addressed?

By row index and column id in the grid's displayed order, which is why the flow reads api.getDisplayedRows() rather than the source array: sorting or filtering changes the indexes, and the displayed rows are what startEditing and selectCells expect.

Related documentation

Related articles

  • Paste from Excel into a Svelte Data Grid - How to wire up clipboard paste so users can drop a copied Excel or Google Sheets block directly into SvGrid - TSV parsing, type coercion, validation, and row growth all covered.
  • A Fill Handle (Drag to Fill) in SvGrid - Build a working spreadsheet-style fill handle on top of SvGrid's cell selection and editing - pointer tracking, range highlighting, series fill, and undo/redo integration all covered.
  • An Editable Select / Dropdown Cell in SvGrid - Build a dropdown cell editor in SvGrid using a Svelte 5 snippet - constrain user input to a fixed option set while keeping sort, filter, and undo working on the underlying value.

Source code (176-programmatic-editing.svelte)

<!-- Documented in: docs/help/editing/start-stop-editing.md -->
<script lang="ts">
  /**
   * 176. Programmatic editing - api.startEditing / stopEditing
   * ----------------------------------------------------------
   * Drive the editor from outside the grid. A toolbar begins editing the
   * active cell, commits or cancels it, and runs a "guided data entry" flow
   * that jumps to the next blank required field and opens its editor - so a
   * form-style entry experience is built entirely on the imperative API.
   */
  import {
    SvGrid,
    tableFeatures,
    rowSortingFeature,
    type GridColumns,
    type SvGridApi,
  } from '@svgrid/grid'

  type Lead = {
    company: string
    contact: string
    email: string
    stage: string
  }

  let rows = $state<Lead[]>([
    { company: 'Northwind', contact: 'Ada Lovelace', email: '', stage: 'New' },
    { company: 'Helios', contact: '', email: '[email protected]', stage: '' },
    { company: 'Vertex', contact: 'Linus T.', email: '', stage: 'Qualified' },
    { company: '', contact: 'Grace H.', email: '[email protected]', stage: 'New' },
    { company: 'Stellar', contact: 'Tim B.', email: '[email protected]', stage: '' },
  ])

  const features = tableFeatures({ rowSortingFeature })
  const REQUIRED = ['company', 'contact', 'email', 'stage']

  let api = $state<SvGridApi<typeof features, Lead> | null>(null)
  let active = $state<{ rowIndex: number; columnId: string } | null>(null)
  let editing = $state(false)

  const columns: GridColumns<Lead> = [
    { field: 'company', header: 'Company', editorType: 'text', width: 160 },
    { field: 'contact', header: 'Contact', editorType: 'text', width: 160 },
    { field: 'email', header: 'Email', editorType: 'text', width: 220 },
    {
      field: 'stage',
      header: 'Stage',
      editorType: 'rich-select',
      width: 150,
      editorOptions: ['New', 'Qualified', 'Proposal', 'Won', 'Lost'],
    },
  ]

  function editActive() {
    if (!api || !active) return
    editing = api.startEditing(active.rowIndex, active.columnId)
  }
  function commit() {
    if (!api) return
    api.stopEditing() // default: commit
    editing = false
  }
  function cancel() {
    if (!api) return
    api.stopEditing(true) // discard
    editing = false
  }

  // Guided entry: find the next blank required cell (row-major from the active
  // cell), select it, and open its editor via the API.
  //
  // IMPORTANT: walk the DISPLAYED rows (`api.getDisplayedRows()`), not the
  // source `rows` array. `startEditing` / `selectCells` address cells by their
  // position in the grid as rendered, so after a sort the displayed order
  // differs from the source order - scanning the source array would open the
  // wrong (already-filled) cell.
  function nextBlank() {
    if (!api) return
    const displayed = api.getDisplayedRows()
    const total = displayed.length * REQUIRED.length
    if (!total) return
    const start = active ? active.rowIndex * REQUIRED.length + REQUIRED.indexOf(active.columnId) + 1 : 0
    for (let k = 0; k < total; k += 1) {
      const flat = (start + k) % total
      const r = Math.floor(flat / REQUIRED.length)
      const columnId = REQUIRED[flat % REQUIRED.length]!
      if (!String((displayed[r] as Record<string, unknown>)[columnId] ?? '').trim()) {
        const colIndex = columns.findIndex((c) => c.field === columnId)
        api.selectCells([[r, colIndex, r, colIndex]])
        active = { rowIndex: r, columnId }
        editing = api.startEditing(r, columnId)
        return
      }
    }
    editing = false
  }

  const blanks = $derived(
    rows.reduce(
      (n, row) =>
        n + REQUIRED.filter((f) => !String((row as Record<string, unknown>)[f] ?? '').trim()).length,
      0,
    ),
  )
</script>

<section class="flex flex-col flex-1 min-h-0 gap-3">
  <div class="flex flex-wrap items-center gap-2 shrink-0">
    <button type="button" class="pe-btn" onclick={editActive} disabled={!active || editing}>Edit active cell</button>
    <button type="button" class="pe-btn pe-primary" onclick={commit} disabled={!editing}>Commit (save)</button>
    <button type="button" class="pe-btn" onclick={cancel} disabled={!editing}>Cancel (discard)</button>
    <button type="button" class="pe-btn pe-accent" onclick={nextBlank}>
      Next blank &amp; edit {#if blanks > 0}<span class="pe-badge">{blanks}</span>{/if}
    </button>
    <span class="text-xs" style="color: var(--sg-muted);">
      {#if active}Active: <code>{active.columnId}</code> · row {active.rowIndex + 1}{:else}Click a cell to make it active{/if}
      {#if editing}<span style="color: #34d399;"> · editing</span>{/if}
    </span>
  </div>

  <p class="text-sm shrink-0" style="color: var(--sg-fg);">
    Everything here is <code>api.startEditing(rowIndex, columnId)</code> and
    <code>api.stopEditing(cancel?)</code> - no double-click needed.
    <strong>Next blank &amp; edit</strong> walks the {blanks === 0 ? 'now-complete' : ''} required
    fields and opens each empty one, a form-style flow on top of the grid.
  </p>

  <div class="flex-1 min-h-0">
    <SvGrid responsive={true}
      columnResize
      data={rows}
      columns={columns}
      features={features}
      showRowNumbers={true}
      enableInlineEditing={true}
      enableCellSelection={true}
      rowHeight={38}
      containerHeight="100%"
      fitColumns={true}
      onApiReady={(next) => (api = next)}
      onActiveCellChange={(e) => (active = { rowIndex: e.rowIndex, columnId: e.columnId })}
    />
  </div>
</section>

<style>
  .pe-btn {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    padding: 6px 12px;
    border-radius: 7px;
    font-size: 12.5px;
    font-weight: 600;
    color: var(--sg-fg);
    background: var(--sg-header-bg);
    border: 1px solid var(--sg-border);
    cursor: pointer;
    transition: border-color 120ms ease, background-color 120ms ease, opacity 120ms ease;
  }
  .pe-btn:hover:not(:disabled) { border-color: var(--sg-accent, #3b82f6); }
  .pe-btn:disabled { opacity: 0.45; cursor: default; }
  .pe-primary { background: var(--sg-accent, #3b82f6); color: var(--sg-on-accent, #fff); border-color: transparent; }
  .pe-accent { border-color: var(--sg-accent, #3b82f6); color: var(--sg-accent, #3b82f6); }
  .pe-badge {
    display: inline-flex; min-width: 18px; justify-content: center;
    padding: 0 5px; border-radius: 9px; font-size: 10px;
    background: var(--sg-accent, #3b82f6); color: var(--sg-on-accent, #fff);
  }
</style>

View this example on GitHub

More Editing examples

  • Inline editing - Typed editors (text/number/checkbox/date) with dirty tracking + save.
  • Editor types + custom slot - Built-in select / rich-select / textarea editors plus a custom `cellEditor` snippet (a range slider) for cases the built-ins do not cover.
  • List + chips editors - Two built-in editors with single & multi-select: dropdown (list) and removable tokens (chips), with options or free-form.
  • Async editor options - editorOptions can return a Promise - for the whole column, or per row for a cascade. The dropdown shows Loading… while the request is in flight, results are cached (per column, or per column+row) so reopening never refetches, and api.refreshEditorOptions() invalidates when the server list changes.
  • Custom cell editors - Three hand-rolled editors: native colour picker bound to a tag swatch, 5-star rating, emoji feedback mood. All write back through api.setCellValue.