bo-grid v3.1.0

Editing & selection

.md

Custom editors

Replace a column's editor with your own component, parse typed or pasted text into values, and share settings across columns.

The built-in editors cover text, numbers, dates and dropdowns. When a column needs something else (a stepper, a star picker, a tag list), give it an editor component. When the editor is fine but the text needs reading (a currency sign, thousands separators), give it a parse function.

Double-click Qty for a stepper editor (↑ and ↓ step by 100, Enter saves), or type $1,250.50 into Price: parse turns it into 1250.5.

custom-editors.svelte GitHub ↗

An editor component

A column’s editor replaces the built-in input. It receives value, row, column, seed (the key that started type-to-edit, or null) and two callbacks: commit(value) saves any value as it is (no parsing) and cancel() closes without saving. Escape cancels, and so does focus leaving the editor; the grid focuses the editor’s first focusable element when it opens.

<!-- Stepper.svelte -->
<script lang="ts">
  import { untrack } from 'svelte';
  import type { CellEditorProps } from 'bo-grid';

  let { value, commit }: CellEditorProps = $props();
  let qty = $state(untrack(() => Number(value) || 0));
</script>

<span role="spinbutton" tabindex="0" aria-label="Quantity" aria-valuenow={qty}
  onkeydown={(e) => {
    if (e.key === 'ArrowUp') qty += 100;
    else if (e.key === 'ArrowDown') qty = Math.max(0, qty - 100);
    else if (e.key === 'Enter') commit(qty);
  }}>{qty}</span>
<button type="button" onclick={() => commit(qty)}>OK</button>
import Stepper from './Stepper.svelte';

{ type: 'number', key: 'qty', header: 'Qty', editable: true, editor: Stepper }

The committed value still goes through the column’s validate, then to onCellEdit. An editor also makes a display widget editable: rating, tags, badge and the other display types are read-only until they have one.

import RatingEditor from './RatingEditor.svelte';

{ type: 'rating', key: 'rating', header: 'Rating', editable: true, editor: RatingEditor }

The spreadsheet example has a full star-picker editor: Spreadsheet.

Parsing typed and pasted text

parse(raw, row) turns text into the stored value. It runs for the built-in editor, paste and the fill handle, in place of the built-in number and date conversion. Return undefined (or NaN) to refuse the input:

{
  type: 'price', key: 'price', header: 'Price', editable: true,
  parse: (raw) => (/d/.test(raw) ? Number(raw.replace(/[^d.-]/g, '')) : undefined),
}

validate then sees the parsed value.

Settings shared by every column

defaultColumn applies settings under every column. A column’s own fields win, and so does a registered cellType:

<Grid {rows} {columns} height={480} defaultColumn={{ resizable: false, tooltip: true }} />

To reuse a whole column recipe (format, editor, filter, width) under a name, register it with cellTypes; see Column types.

Reference

Column options from the types
editor Component<CellEditorProps>

Svelte component that edits this cell in place of the built-in input. Makes any column type editable (with editable: true), including display widgets like rating or tags. It calls commit(value) to save — any value, no parsing — or cancel(); Escape cancels.

Example
{ type: 'rating', key: 'score', header: 'Score', editable: true, editor: StarPicker }
parse (raw: string, row: GridRow) => unknown

Turn typed or pasted text into the stored value (runs before the built-in number/date coercion, which it replaces). Return undefined or a non-finite number to reject the input.

Example
{ type: 'number', key: 'qty', header: 'Qty', editable: true, parse: (raw) => Number(raw.replace(/[,\s]/g, '')) }
validate (value: unknown, row: GridRow) => boolean

Validate an edited value before it commits (inline edit, custom editor, paste, fill). Return false to reject and keep the old value. Receives the parsed value.

Example
{ type: 'number', key: 'qty', header: 'Qty', editable: true, decimals: 0, validate: (v) => Number(v) > 0 && Number(v) % 100 === 0 }
editable boolean Default false

Allow inline editing (double-click, Enter or F2 on the focused cell, or type to replace). Number types edit as numbers, date as a date. Display types (badge, boolean, rating, progress, tags, link, avatar, relative, sparkline, custom) edit only with an editor; a computed (value) column never does.

options string[] Default undefined

Editable choices: when set, editing renders a <select> of these options instead of a text input (enum/status columns).

Example
{ type: 'text', key: 'side', header: 'Side', editable: true, options: ['Buy', 'Sell'] }
Grid props from the types
defaultColumn DefaultColumn Default undefined

Settings applied under every column — its own fields (and a registered cellType) win.

Example
<Grid {rows} {columns} height={480} defaultColumn={{ minWidth: 72, sortable: false }} />
cellTypes Record<string, CellTypeDef> Default undefined

Reusable column types, by name. A column with cellType: 'name' gets that entry's defaults (format, compare, align, filter, width, a component renderer…) under its own fields, and the built-in type named by extends for sorting, filtering and export.

Example
<Grid {rows} {columns} height={480}
  cellTypes={{ qty: { extends: 'number', decimals: 0, width: 80, format: (v) => Number(v).toLocaleString() } }} />
onCellEdit (e: CellEditEvent) => void

Called when an editable cell is committed. Update your row data in here.

Example
<Grid {rows} {columns} height={480} onCellEdit={({ row, column, value }) => (row[column.key] = value)} />