# 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.

Example: A custom editor and a parser (custom-editors.svelte)

```svelte
<script lang="ts">
  import { Grid, type ColumnDef } from 'bo-grid';
  import { quotes } from './data';
  import Stepper from './custom-editors-stepper.svelte';

  const columns: ColumnDef[] = [
    { type: 'text', key: 'symbol', header: 'Symbol', width: 100 },
    // A custom editor component instead of the built-in input.
    { type: 'number', key: 'qty', header: 'Qty (lots of 100)', width: 230, decimals: 0, editable: true, editor: Stepper },
    // `parse` reads typed or pasted text: "1,250.5" or "$1250.5" become 1250.5.
    {
      type: 'price', key: 'price', header: 'Price', width: 130, editable: true,
      parse: (raw) => (/\d/.test(raw) ? Number(raw.replace(/[^\d.-]/g, '')) : undefined),
    },
    // `options` swaps the text input for a dropdown.
    { type: 'text', key: 'side', header: 'Side', flex: 1, editable: true, options: ['Buy', 'Sell'] },
  ];
  let rows = $state(quotes().map((q, i) => ({ ...q, qty: (i % 5) * 100 + 100, side: i % 3 ? 'Buy' : 'Sell' })));
</script>

<Grid {rows} {columns} height={280} defaultColumn={{ resizable: false }} onCellEdit={(e) => (e.row[e.column.key] = e.value)} />
```

data.ts:

```ts
// Small, fixed data for the docs' examples: every example reads the same
// symbols, so a reader compares features, not datasets.
// A type, not an interface: it then fits GridRow's index signature.
export type Quote = {
  id: number;
  symbol: string;
  name: string;
  sector: string;
  price: number;
  change: number;
  volume: number;
};

const BASE: Omit<Quote, 'id'>[] = [
  { symbol: 'VNM', name: 'Vinamilk', sector: 'Consumer', price: 68.5, change: 1.2, volume: 1_204_000 },
  { symbol: 'FPT', name: 'FPT Corp', sector: 'Technology', price: 132, change: -0.4, volume: 860_500 },
  { symbol: 'HPG', name: 'Hoa Phat', sector: 'Materials', price: 27.35, change: 0.74, volume: 3_412_900 },
  { symbol: 'VCB', name: 'Vietcombank', sector: 'Financials', price: 91.5, change: 0.55, volume: 1_986_200 },
  { symbol: 'MWG', name: 'Mobile World', sector: 'Consumer', price: 51.4, change: -1.1, volume: 2_250_400 },
  { symbol: 'SSI', name: 'SSI Securities', sector: 'Financials', price: 33.3, change: -0.45, volume: 4_104_700 },
  { symbol: 'VIC', name: 'Vingroup', sector: 'Real estate', price: 44.15, change: -0.11, volume: 1_530_100 },
  { symbol: 'GAS', name: 'PV Gas', sector: 'Energy', price: 71.8, change: 0.3, volume: 645_800 },
  { symbol: 'MSN', name: 'Masan', sector: 'Consumer', price: 76.2, change: 2.05, volume: 1_120_300 },
  { symbol: 'TCB', name: 'Techcombank', sector: 'Financials', price: 23.9, change: 0.84, volume: 5_870_000 },
  { symbol: 'VHM', name: 'Vinhomes', sector: 'Real estate', price: 41.6, change: -0.72, volume: 2_904_600 },
  { symbol: 'POW', name: 'PV Power', sector: 'Energy', price: 12.25, change: 1.66, volume: 6_315_200 },
];

/** A fresh copy of the quotes, so one example's edits never reach another. */
export function quotes(): Quote[] {
  return BASE.map((q, i) => ({ id: i + 1, ...q }));
}
```

## 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. <kbd>Escape</kbd> cancels, and so does focus leaving the editor; the grid focuses the editor's first focusable element when it opens.

```svelte
<!-- 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>
```

```ts
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.

```ts
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](https://bonguynvan.github.io/bo-grid/examples/sheet/).

## 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:

```ts
{
  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`:

```svelte
<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](https://bonguynvan.github.io/bo-grid/docs/column-types/).

## Reference

- `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.

  ```ts
  { 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.

  ```ts
  { 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.

  ```ts
  { 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).

  ```ts
  { type: 'text', key: 'side', header: 'Side', editable: true, options: ['Buy', 'Sell'] }
  ```


- `defaultColumn`: `DefaultColumn` (default `undefined`) — Settings applied under every column — its own fields (and a registered `cellType`) win.

  ```svelte
  <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.

  ```svelte
  <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.

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

---
Page: https://bonguynvan.github.io/bo-grid/docs/custom-editors/ · All pages: https://bonguynvan.github.io/bo-grid/llms.txt
