# Edits

> Package `@umriss-ui/table`, version 0.40.0. Demo page: <https://romanhaendler.github.io/umriss-ui/table/edits/>. Every page in one line: [llms.txt](https://romanhaendler.github.io/umriss-ui/table/llms.txt); every page in full: [llms-full.txt](https://romanhaendler.github.io/umriss-ui/table/llms-full.txt).

Let people change a value in its cell. The table reports each edit, and the application writes it into its rows.

```ts
import { useTable } from "@umriss-ui/table";
```

Demo page: https://romanhaendler.github.io/umriss-ui/table/edits/

An edit is reported, never applied: the application owns the rows. `validate` keeps the editor open with its message until the draft passes.

By default each cell is reported as it is left (`onCellEdit`) - for another cell, or by a click outside the table. `editMode="row"` opens a whole row as one draft and reports it only when it is saved (`onRowSave`); a row with a draft is left only by saving or discarding it ([ADR-0036](https://github.com/romanhaendler/umriss-ui/blob/main/docs/adr/0036-a-grid-edits-on-the-first-click-and-saves-rows-on-purpose.md)).

`onRowAdd` puts up "New row" - in the toolbar, or beneath the table without one - and `onRowDelete` a Delete that asks inside its row. Both work in either mode.

## Examples

### Edit a comments column

`edit="text"` lets a column edit; the table reports each edit through `onCellEdit` and the application writes it into its rows.

```tsx
import { useState } from "react";
import { useTable } from "@umriss-ui/table";

interface Tour {
  id: string;
  driver: string;
  stops: number;
  comment: string;
}

const TOURS: Tour[] = [
  { id: "T-01", driver: "Martin Hale", stops: 11, comment: "" },
  { id: "T-02", driver: "Nadia Petrova", stops: 9, comment: "Van swapped at 10:40" },
  { id: "T-03", driver: "Owen Carter", stops: 5, comment: "" },
];

export default function CommentsColumn() {
  const [tours, setTours] = useState(TOURS);
  const { Table, Column } = useTable(tours, { rowKey: (t) => t.id });
  return (
    <Table
      grid
      ariaLabel="Today's tours"
      onCellEdit={({ rowKey, value }) => setTours((all) => all.map((t) => (t.id === rowKey ? { ...t, comment: String(value) } : t)))}
    >
      <Column value="id" label="Tour" rowHeader />
      <Column value="driver" label="Driver" />
      <Column value="stops" label="Stops" />
      <Column value="comment" label="Comment" edit="text" width={280} />
    </Table>
  );
}
```

### Check an edit before it is reported

Each editing column gets the core field for its value; `validate` keeps the editor open with its message until the draft passes.

```tsx
import { useState } from "react";
import { Stack, Text } from "@umriss-ui/core";
import { useTable } from "@umriss-ui/table";
import type { CellEdit } from "@umriss-ui/table";

type Role = "Developer" | "Designer" | "Product manager" | "QA engineer";

interface Person {
  id: string;
  name: string;
  team: string;
  capacity: number;
  role: Role;
  availableFrom: Date;
}

const PEOPLE: Person[] = [
  { id: "maya", name: "Maya Lindgren", team: "Web", capacity: 32, role: "Product manager", availableFrom: new Date(2026, 2, 2) },
  { id: "arjun", name: "Arjun Mehta", team: "Web", capacity: 40, role: "Developer", availableFrom: new Date(2026, 2, 23) },
  { id: "noah", name: "Noah Fischer", team: "Web", capacity: 24, role: "Designer", availableFrom: new Date(2026, 2, 16) },
  { id: "david", name: "David Kowalski", team: "Apps", capacity: 20, role: "QA engineer", availableFrom: new Date(2026, 2, 16) },
];

export default function ValidatedEdits() {
  const [people, setPeople] = useState(PEOPLE);
  const [last, setLast] = useState<CellEdit<Person> | null>(null);
  const { Table, Column } = useTable(people, { rowKey: (p) => p.id });

  const apply = (edit: CellEdit<Person>) => {
    setPeople((all) => all.map((p) => (p.id === edit.rowKey ? { ...p, [edit.columnId]: edit.value } : p)));
    setLast(edit);
  };

  return (
    <Stack gap={2}>
      <Table grid ariaLabel="Capacity" onCellEdit={apply}>
        <Column value="name" label="Person" rowHeader />
        <Column value="team" label="Team" />
        <Column
          value="capacity"
          label="Capacity"
          width={140}
          edit="number"
          validate={(value) =>
            value === null ? "A person needs a capacity" : value < 4 || value > 40 ? "Between 4 and 40 hours a week" : undefined
          }
        />
        <Column value="role" label="Role" edit="select" editOptions={["Developer", "Designer", "Product manager", "QA engineer"]} width={170} />
        <Column value="availableFrom" label="Available from" format="date" edit="date" width={160} />
      </Table>
      <Text size="xs" tone="muted">
        {last ? `Last edit: ${last.row.name}, ${last.columnId}` : "No edit yet."}
      </Text>
    </Stack>
  );
}
```

### Save a row on purpose

`editMode="row"` opens every cell of a row that edits at once. Nothing is reported until Save or Enter; `onRowSave` hands over only the columns that changed.

```tsx
import { useState } from "react";
import { Stack, Text } from "@umriss-ui/core";
import { useTable } from "@umriss-ui/table";
import type { RowSave } from "@umriss-ui/table";

interface Project {
  id: string;
  name: string;
  phase: string;
  owner: string;
  days: number;
}

const PROJECTS: Project[] = [
  { id: "P-1", name: "Checkout redesign", phase: "Build", owner: "Ines Duarte", days: 40 },
  { id: "P-2", name: "Mobile app", phase: "Discovery", owner: "Tomasz Wrona", days: 15 },
  { id: "P-3", name: "Data warehouse", phase: "Launch", owner: "Amira Haddad", days: 60 },
];

export default function SaveARowOnPurpose() {
  const [projects, setProjects] = useState(PROJECTS);
  const [last, setLast] = useState<RowSave<Project> | null>(null);
  const { Table, Column } = useTable(projects, { rowKey: (p) => p.id });

  const save = (saved: RowSave<Project>) => {
    setProjects((all) => all.map((p) => (p.id === saved.rowKey ? { ...p, ...saved.changes } : p)));
    setLast(saved);
  };

  return (
    <Stack gap={2}>
      <Table grid editMode="row" ariaLabel="Projects" onRowSave={save}>
        <Column value="name" label="Project" rowHeader />
        <Column value="phase" label="Phase" edit="select" editOptions={["Discovery", "Build", "Launch"]} width={140} />
        <Column value="owner" label="Owner" edit="text" />
        <Column value="days" label="Budget (days)" edit="number" width={130} />
      </Table>
      <Text size="xs" tone="muted">
        {last ? `Saved ${last.row.name}: ${Object.keys(last.changes).join(", ")}` : "Nothing saved yet."}
      </Text>
    </Stack>
  );
}
```

### Check a whole row before it is saved

On Save every open cell runs its `validate`; each message stands beneath its field, and the row stays open until all of them pass. A click into another row is refused while the draft is open.

```tsx
import { useState } from "react";
import { useTable } from "@umriss-ui/table";

interface Offer {
  id: string;
  product: string;
  unit: string;
  price: number | null;
  minimum: number | null;
  maximum: number | null;
}

const OFFERS: Offer[] = [
  { id: "o1", product: "Printer paper A4", unit: "box", price: 24.5, minimum: 5, maximum: 200 },
  { id: "o2", product: "Toner cartridge", unit: "piece", price: 68, minimum: 1, maximum: 40 },
  { id: "o3", product: "Desk lamp", unit: "piece", price: 39.9, minimum: 2, maximum: 25 },
];

const required = (label: string) => (value: number | null) => (value === null ? `${label} is required` : undefined);

export default function CheckAWholeRow() {
  const [offers, setOffers] = useState(OFFERS);
  const { Table, Column } = useTable(offers, { rowKey: (o) => o.id });
  return (
    <Table
      grid
      editMode="row"
      ariaLabel="Price list"
      onRowSave={({ rowKey, changes }) => setOffers((all) => all.map((o) => (o.id === rowKey ? { ...o, ...changes } : o)))}
    >
      <Column value="product" label="Product" rowHeader />
      <Column value="unit" label="Unit" width={80} />
      <Column
        value="price"
        label="Price"
        edit="number"
        width={130}
        validate={(value) => (value === null ? "A price is required" : value < 0 ? "Not below 0" : undefined)}
      />
      <Column value="minimum" label="Minimum order" edit="number" width={150} validate={required("A minimum order")} />
      <Column value="maximum" label="Maximum order" edit="number" width={150} validate={required("A maximum order")} />
    </Table>
  );
}
```

### Keep a list: add, change and delete rows

`onRowAdd` puts up "New row" in the toolbar - an empty row draft above the rows, whatever the sort; `newRow` gives its first values. `onRowDelete` adds a Delete that asks inside its row. The application owns the rows: it inserts, changes and removes them itself.

```tsx
import { useState } from "react";
import { Search, Toolbar, useTable } from "@umriss-ui/table";

type Kind = "Spare part" | "Consumable" | "Tool";

interface Item {
  id: string;
  name: string;
  kind: Kind;
  stock: number | null;
  location: string;
}

const ITEMS: Item[] = [
  { id: "i1", name: "Burner nozzle 4 mm", kind: "Spare part", stock: 6, location: "Store A-3" },
  { id: "i2", name: "Refractory mortar", kind: "Consumable", stock: 14, location: "Yard" },
  { id: "i3", name: "Thermocouple type K", kind: "Spare part", stock: 3, location: "Store A-1" },
  { id: "i4", name: "Torque wrench 40-200 Nm", kind: "Tool", stock: 2, location: "Workshop" },
];

let next = ITEMS.length;

export default function KeepAList() {
  const [items, setItems] = useState(ITEMS);
  const { Table, Column } = useTable(items, { rowKey: (i) => i.id });
  return (
    <Table
      grid
      editMode="row"
      ariaLabel="Stores"
      newRow={() => ({ kind: "Spare part", stock: 0 })}
      onRowSave={({ rowKey, changes }) => setItems((all) => all.map((i) => (i.id === rowKey ? { ...i, ...changes } : i)))}
      onRowAdd={({ values }) => setItems((all) => [...all, { id: `i${++next}`, ...(values as Omit<Item, "id">) }])}
      onRowDelete={({ rowKey }) => setItems((all) => all.filter((i) => i.id !== rowKey))}
    >
      <Toolbar>
        <Search />
      </Toolbar>
      <Column value="name" label="Item" rowHeader edit="text" validate={(value) => (value ? undefined : "An item needs a name")} />
      <Column value="kind" label="Kind" edit="select" editOptions={["Spare part", "Consumable", "Tool"]} width={150} />
      <Column value="stock" label="In stock" edit="number" width={110} validate={(value) => (value === null || value < 0 ? "0 or more" : undefined)} />
      <Column value="location" label="Location" edit="text" width={150} />
    </Table>
  );
}
```

### Edit a dense table

`density="compact"` holds for a row draft too: its fields and buttons are a step smaller, and a row with actions stands as tall as one without.

```tsx
import { useState } from "react";
import { useTable } from "@umriss-ui/table";

interface Room {
  id: string;
  name: string;
  seats: number;
  floor: number;
}

const ROOMS: Room[] = [
  { id: "r1", name: "Harbour", seats: 8, floor: 2 },
  { id: "r2", name: "Orchard", seats: 12, floor: 3 },
  { id: "r3", name: "Lantern", seats: 4, floor: 3 },
];

export default function EditADenseTable() {
  const [rooms, setRooms] = useState(ROOMS);
  const { Table, Column } = useTable(rooms, { rowKey: (r) => r.id });
  return (
    <Table
      grid
      editMode="row"
      density="compact"
      ariaLabel="Meeting rooms"
      onRowSave={({ rowKey, changes }) => setRooms((all) => all.map((r) => (r.id === rowKey ? { ...r, ...changes } : r)))}
      onRowDelete={({ rowKey }) => setRooms((all) => all.filter((r) => r.id !== rowKey))}
    >
      <Column value="name" label="Room" rowHeader edit="text" />
      <Column value="seats" label="Seats" edit="number" width={120} />
      <Column value="floor" label="Floor" edit="number" width={120} />
    </Table>
  );
}
```

## Keyboard

| Key | Action |
|---|---|
| `Tab (while editing)` | Commits the edit and moves to the next cell that edits; in a row draft, walks its fields and buttons. |
| `Enter (row draft)` | Saves the row. |
| `Escape (row draft)` | Discards the row's changes. |

## Props on this page

From `TableProps` — the full table stands on [First table](https://romanhaendler.github.io/umriss-ui/table/first-table/#type-TableProps).

| Prop | Type | Default | Description |
|---|---|---|---|
| [`density`](https://romanhaendler.github.io/umriss-ui/table/first-table/#TableProps-density) | `"regular" \| "compact"` | the density of the `UmrissProvider`, else `"regular"` | How tightly the rows stand. |
| [`grid`](https://romanhaendler.github.io/umriss-ui/table/first-table/#TableProps-grid) | `boolean` | — | Grid mode: the table is one tab stop, and the arrows walk its cells - Home and End in the row, Ctrl+Home and Ctrl+End to the table's ends, PageUp and PageDown by the rows in view. A cell's own controls are reached with Enter or F2 and left with Escape; a column with `edit` is edited in place. Without it the table stays a native table. |
| [`editMode`](https://romanhaendler.github.io/umriss-ui/table/first-table/#TableProps-editMode) | `"cell" \| "row"` | — | Grid mode ([ADR-0036](https://github.com/romanhaendler/umriss-ui/blob/main/docs/adr/0036-a-grid-edits-on-the-first-click-and-saves-rows-on-purpose.md)): `"cell"` reports every cell as it is left (`onCellEdit`); `"row"` opens every cell of a row that edits at once, as one **Row draft**, and reports it only when the row is saved on purpose (`onRowSave`) - by its Save button or Enter; Escape or Discard drops it. A row with a draft is left only by saving or discarding it. |
| [`newRow`](https://romanhaendler.github.io/umriss-ui/table/first-table/#TableProps-newRow) | `() => Partial<Z>` | — | What a new row starts with - the values the columns read before anything is typed. Without it, nothing. |
| [`children`](https://romanhaendler.github.io/umriss-ui/table/first-table/#TableProps-children) | `ReactNode` | — | Columns, `RowDetail`, `RowActions` and the unbound parts. |

#### Events

| Prop | Type | Default | Description |
|---|---|---|---|
| [`onCellEdit`](https://romanhaendler.github.io/umriss-ui/table/first-table/#TableProps-onCellEdit) | `(edit: `[`CellEdit`](https://romanhaendler.github.io/umriss-ui/table/first-table/#type-CellEdit)`<Z>) => void` | — | Grid mode: an edit the user committed. The table applies nothing; the cell shows the new value when the rows passed in carry it. |
| [`onRowSave`](https://romanhaendler.github.io/umriss-ui/table/first-table/#TableProps-onRowSave) | `(save: `[`RowSave`](https://romanhaendler.github.io/umriss-ui/table/first-table/#type-RowSave)`<Z>) => void` | — | `editMode="row"`: a saved Row draft, with the columns it changed. |
| [`onRowAdd`](https://romanhaendler.github.io/umriss-ui/table/first-table/#TableProps-onRowAdd) | `(add: `[`RowAdd`](https://romanhaendler.github.io/umriss-ui/table/first-table/#type-RowAdd)`) => void` | — | Grid mode: puts up a "New row" button - in the table toolbar, or beneath the table without one. It opens an empty Row draft above the rows; saved, it is reported whole. The row appears when the rows passed in carry it. |
| [`onRowDelete`](https://romanhaendler.github.io/umriss-ui/table/first-table/#TableProps-onRowDelete) | `(remove: `[`RowDelete`](https://romanhaendler.github.io/umriss-ui/table/first-table/#type-RowDelete)`<Z>) => void` | — | Grid mode: a Delete button on every row, which asks inside its row before the delete is reported. The table removes nothing. |

#### Accessibility

| Prop | Type | Default | Description |
|---|---|---|---|
| [`ariaLabel`](https://romanhaendler.github.io/umriss-ui/table/first-table/#TableProps-ariaLabel) | `string` | — | Accessible name of the table (not of the outer frame). |

From `TableOptions` — the full table stands on [First table](https://romanhaendler.github.io/umriss-ui/table/first-table/#type-TableOptions).

| Prop | Type | Default | Description |
|---|---|---|---|
| [`rowKey`](https://romanhaendler.github.io/umriss-ui/table/first-table/#TableOptions-rowKey) *required* | `(row: Z) => string` | — | A stable key per row – the basis of selection and expansion. |

From `FieldColumn` — the full table stands on [Column](https://romanhaendler.github.io/umriss-ui/table/column/#type-FieldColumn).

| Prop | Type | Default | Description |
|---|---|---|---|
| [`value`](https://romanhaendler.github.io/umriss-ui/table/column/#FieldColumn-value) *required* | `K` | — | A field of the row. Supplies the column's `id` at the same time. |
| [`format`](https://romanhaendler.github.io/umriss-ui/table/column/#FieldColumn-format) | [`FormatFor`](https://romanhaendler.github.io/umriss-ui/table/column/#type-FormatFor)`<Z[K]>` | — | A standard presentation by name: `"percent"`, `"count"` or `{ decimals }` for numbers, `"date"`, `"time"` or `"dateTime"` for points in time. Alignment, sorting, export and footer stay with the value. |
| [`edit`](https://romanhaendler.github.io/umriss-ui/table/column/#FieldColumn-edit) | [`EditFor`](https://romanhaendler.github.io/umriss-ui/table/column/#type-EditFor)`<Z[K], Z>` | — | Edits the column's cells in place in grid mode: Enter, F2 or typing starts it, Enter or Tab commits, Escape cancels. The table applies nothing - the edit is reported through the table's `onCellEdit`. From `EditOptions`. |
| [`editOptions`](https://romanhaendler.github.io/umriss-ui/table/column/#FieldColumn-editOptions) | `readonly `[`Present`](https://romanhaendler.github.io/umriss-ui/table/column/#type-Present)`<Z[K]>[]` | the values that occur | What `edit="select"` offers. From `EditOptions`. |
| [`validate`](https://romanhaendler.github.io/umriss-ui/table/column/#FieldColumn-validate) | `(value: Z[K] \| null, row: Z) => string \| null \| undefined` | — | Checks a draft before it is reported: a message keeps the editor open and stands beneath the field, as in a `FormField`; nothing lets it through. From `EditOptions`. |

From `ColumnBase` — the full table stands on [Column](https://romanhaendler.github.io/umriss-ui/table/column/#type-ColumnBase).

| Prop | Type | Default | Description |
|---|---|---|---|
| [`label`](https://romanhaendler.github.io/umriss-ui/table/column/#ColumnBase-label) *required* | `string` | — | Names the column in its header, in the column menu and in the header row of the export. |
| [`rowHeader`](https://romanhaendler.github.io/umriss-ui/table/column/#ColumnBase-rowHeader) | `boolean` | — | This column names the row: row header cells, never hideable, the column `stickyRowHeader` pins, and the name by which the row's selection, expander and actions are read out. At most one per table. |
| [`width`](https://romanhaendler.github.io/umriss-ui/table/column/#ColumnBase-width) | `number` | the browser's | Initial width in pixels. |

From `ToolbarProps` — the full table stands on [Toolbar](https://romanhaendler.github.io/umriss-ui/table/toolbar/#type-ToolbarProps).

| Prop | Type | Default | Description |
|---|---|---|---|
| [`children`](https://romanhaendler.github.io/umriss-ui/table/toolbar/#ToolbarProps-children) | `ReactNode` | — | Search, column menu, export – on the left in the table toolbar, before the conditions. |

## Known limits

- No cell range selection and no undo ([ADR-0032](https://romanhaendler.github.io/umriss-ui/what-umriss-ui-is-not/)).

What umriss deliberately does not build, and why: [ADR-0032](https://romanhaendler.github.io/umriss-ui/what-umriss-ui-is-not/).
