# Inline editing

> Edit cells in place with an editor matched to the column type, validate the value, and write it back to your rows.

Mark a column `editable: true` and its cells can be edited in place. The grid is controlled: it never writes to your data itself, but reports each change through `onCellEdit`, where you update the row (or send the change to a server first).

Double-click a cell, press <kbd>Enter</kbd> on it, or select it and start typing. Status opens a dropdown, and Qty refuses a negative number.

Example: Editable cells (editing.svelte)

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

  const STATUS = ['Watch', 'Buy', 'Hold', 'Sell'];
  const columns: ColumnDef[] = [
    { type: 'text', key: 'symbol', header: 'Symbol', width: 100 },
    // A dropdown editor: the column has `options`.
    { type: 'text', key: 'status', header: 'Status', width: 110, editable: true, options: STATUS },
    // A number editor that refuses negative quantities.
    { type: 'number', key: 'qty', header: 'Qty', decimals: 0, editable: true, validate: (v) => Number(v) >= 0 },
    { type: 'price', key: 'price', header: 'Price', editable: true },
    { type: 'text', key: 'note', header: 'Note', flex: 1, editable: true },
  ];
  let rows = $state(quotes().map((q, i) => ({ ...q, status: STATUS[i % 4], qty: (i + 1) * 100, note: '' })));
  let last = $state('Double-click a cell, or select one and start typing.');
</script>

<p style="margin: 0 0 10px; opacity: 0.8">{last}</p>
<Grid
  {rows}
  {columns}
  height={280}
  onCellEdit={(e) => {
    e.row[e.column.key] = e.value;
    last = `${e.row.symbol} · ${e.column.header} → ${String(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 }));
}
```

## Making a column editable

```svelte
<script lang="ts">
  import { Grid, type ColumnDef } from 'bo-grid';

  const columns: ColumnDef[] = [
    { type: 'text', key: 'symbol', header: 'Symbol' },
    { type: 'number', key: 'qty', header: 'Qty', editable: true },
    { type: 'text', key: 'note', header: 'Note', editable: true },
  ];
  let rows = $state(initialRows);
</script>

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

Keep the rows in `$state` so the cell shows the new value once you write it. The event carries the `row`, the `column` and the new `value`.

## Opening and closing the editor

- Double-click a cell, or press <kbd>Enter</kbd> or <kbd>F2</kbd> on the focused cell.
- Start typing on a focused cell: the editor opens with that character, as in a spreadsheet. (Date and dropdown editors open with <kbd>Enter</kbd> or a double-click.)
- <kbd>Enter</kbd> or leaving the cell saves; <kbd>Tab</kbd> saves and moves to the next cell (on to the next row at the end of one); <kbd>Escape</kbd> cancels. The grid keeps the keyboard afterwards.
- Picking an option in a dropdown with the pointer saves at once; with the arrow keys, <kbd>Enter</kbd> saves.
- What you type is yours until you save: a live update to the cell does not overwrite it.

## The editor for each type

The editor matches the column:

| Column | Editor | `e.value` |
| --- | --- | --- |
| Number types (`number`, `price`, `percent`, `volume`, `currency`, `heatmap`) | a text input with a numeric keypad, so `-` and `.` type as expected | a number; a blank, or text that is not a number, is refused |
| `date` | the browser's date picker | epoch milliseconds |
| Any column with `options` | a dropdown | the chosen string |
| Everything else | text input | the typed string |

Display widgets (`badge`, `tags`, `rating`, `progress` and others) and computed columns (`value`) are not editable by themselves; give a display column an `editor` to edit it. See [Custom editors](https://bonguynvan.github.io/bo-grid/docs/custom-editors/).

## Dropdowns

Give an editable column `options` to pick from a list instead of typing:

```ts
{ type: 'text', key: 'status', header: 'Status', editable: true, options: ['New', 'Active', 'Closed'] }
```

## Validation

`validate(value, row)` runs before a value is saved; return `false` to refuse it and keep the old value. It sees the parsed value (a number for a number column), and it applies to paste and the fill handle too:

```ts
{ type: 'number', key: 'qty', header: 'Qty', editable: true, validate: (v) => Number(v) >= 0 }
```

To turn typed text into a value yourself (strip a currency sign, read "1.5k"), use `parse`; see [Custom editors](https://bonguynvan.github.io/bo-grid/docs/custom-editors/).

## Undo, paste and fill

Edits, pastes and fills undo with <kbd>Ctrl/⌘</kbd>+<kbd>Z</kbd> and redo with <kbd>Ctrl/⌘</kbd>+<kbd>Y</kbd> (or <kbd>Ctrl/⌘</kbd>+<kbd>Shift</kbd>+<kbd>Z</kbd>); a paste or a fill is one step. Paste and the fill handle write through the same validation and call `onCellEdit` for every cell they change. See [Cell selection & clipboard](https://bonguynvan.github.io/bo-grid/docs/selection/).

## A full spreadsheet

Dropdowns, parsed salaries, a date editor, the fill handle and saved layouts together:

Live example: https://bonguynvan.github.io/bo-grid/examples/sheet/ (source: https://github.com/bonguynvan/bo-grid/blob/main/src/demo/examples/)

## Reference

- `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)} />
  ```

- `fillHandle`: `boolean` (default `false`) — Show an Excel-style fill handle at the selection's bottom-right corner; drag it to copy the selected value(s) across the extended range (editable columns only). In-memory mode only.

- `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'] }
  ```

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

- `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, '')) }
  ```

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

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