# Undo/Redo Plugin

> Add undo/redo support for cell edits.

The Undo/Redo plugin tracks all cell edits and lets users revert or replay changes with familiar keyboard shortcuts (Ctrl+Z / Ctrl+Y). It maintains an in-memory history stack with configurable depth—perfect for data entry workflows where mistakes happen.

> ⚠️ **Required Dependency:** This plugin requires [EditingPlugin](/grid/plugins/editing.md) to be loaded first. UndoRedo tracks the edit history that EditingPlugin creates.

## Installation

```ts
import '@toolbox-web/grid/features/editing';
import '@toolbox-web/grid/features/undo-redo';
```

## Basic Usage

Enable both features and history tracking is automatic—every cell edit is recorded, and users can navigate back and forth through the edit history.

#### TypeScript

```ts
import { queryGrid } from '@toolbox-web/grid';

const grid = queryGrid('tbw-grid');
grid.gridConfig = {
  columns: [
    { field: 'name', header: 'Name', editable: true },
    { field: 'price', header: 'Price', type: 'number', editable: true },
    { field: 'quantity', header: 'Quantity', type: 'number', editable: true },
  ],
  features: {
    editing: 'dblclick',
    undoRedo: { maxHistorySize: 50 },
  },
};
```

#### React

```tsx
import '@toolbox-web/grid-react/features/editing';
import '@toolbox-web/grid-react/features/undo-redo';
import { DataGrid } from '@toolbox-web/grid-react';

function MyGrid({ data }) {
  return (
    <DataGrid
      rows={data}
      columns={[
        { field: 'name', header: 'Name', editable: true },
        { field: 'price', header: 'Price', type: 'number', editable: true },
        { field: 'quantity', header: 'Quantity', type: 'number', editable: true },
      ]}
      editing={{ editOn: 'dblclick' }}
      undoRedo={{ maxHistorySize: 50 }}
    />
  );
}
```

#### Vue

```html
<script setup>
import '@toolbox-web/grid-vue/features/editing';
import '@toolbox-web/grid-vue/features/undo-redo';
import { TbwGrid, TbwGridColumn } from '@toolbox-web/grid-vue';

const data = [
  { name: 'Widget', price: 9.99, quantity: 10 },
  { name: 'Gadget', price: 19.99, quantity: 5 },
];
</script>

<template>
  <TbwGrid
    :rows="data"
    editing="dblclick"
    :undo-redo="{ maxHistorySize: 50 }"
  >
    <TbwGridColumn field="name" header="Name" editable />
    <TbwGridColumn field="price" header="Price" type="number" editable />
    <TbwGridColumn field="quantity" header="Quantity" type="number" editable />
  </TbwGrid>
</template>
```

#### Angular

```typescript
// Feature imports - enable the [editing] and [undoRedo] inputs
import { GridEditingDirective } from '@toolbox-web/grid-angular/features/editing';
import { GridUndoRedoDirective } from '@toolbox-web/grid-angular/features/undo-redo';
import { Component } from '@angular/core';
import { Grid } from '@toolbox-web/grid-angular';
import type { ColumnConfig } from '@toolbox-web/grid';

@Component({
  selector: 'app-my-grid',
  imports: [Grid, GridEditingDirective, GridUndoRedoDirective],
  template: `
    <tbw-grid
      [rows]="rows"
      [columns]="columns"
      [editing]="true"
      [undoRedo]="{ maxHistorySize: 50 }"
      style="height: 400px; display: block;">
    </tbw-grid>
  `,
})
export class MyGridComponent {
  rows = [...];

  columns: ColumnConfig[] = [
    { field: 'name', header: 'Name', editable: true },
    { field: 'price', header: 'Price', type: 'number', editable: true },
    { field: 'quantity', header: 'Quantity', type: 'number', editable: true },
  ];
}
```

## Demo

Edit cells by double-clicking, then use Ctrl+Z / Ctrl+Y to undo/redo. Adjust the max history size with the control.

```ts
// UndoRedoDefaultDemo.astro
import '@toolbox-web/grid';
import { queryGrid } from '@toolbox-web/grid';
import '@toolbox-web/grid/features/editing';
import '@toolbox-web/grid/features/undo-redo';

const container = document.getElementById('undo-redo-default-demo');
if (container) {
  const columns = [
    { field: 'id', header: 'ID', type: 'number' },
    { field: 'name', header: 'Name', editable: true },
    { field: 'quantity', header: 'Quantity', type: 'number', editable: true },
    { field: 'price', header: 'Price', type: 'number', editable: true },
  ];
  const sampleData = [
    { id: 1, name: 'Widget A', quantity: 10, price: 25.99 },
    { id: 2, name: 'Widget B', quantity: 5, price: 49.99 },
    { id: 3, name: 'Widget C', quantity: 20, price: 15.0 },
  ];

  const grid = queryGrid('tbw-grid', container)!;

  function rebuild(maxHistorySize = 100) {
    grid.gridConfig = {
      columns,
      features: { editing: true, undoRedo: { maxHistorySize } },
    };
    grid.rows = [...sampleData];
  }

  rebuild();

  container.addEventListener('control-change', ((e: CustomEvent) => {
    rebuild(e.detail.allValues.maxHistorySize as number);
  }) as EventListener);
}
```

## Configuration Options

See [`UndoRedoConfig`](./Interfaces/UndoRedoConfig/) for the full list of options and defaults.

## Keyboard Shortcuts

| Shortcut                              | Action |
| ------------------------------------- | ------ |
| `Ctrl+Z` / `Cmd+Z`                    | Undo   |
| `Ctrl+Y` / `Cmd+Y` / `Cmd+Shift+Z`   | Redo   |

## Programmatic API

```ts
const plugin = grid.getPluginByName('undoRedo');

plugin.undo();
plugin.redo();
const canUndo = plugin.canUndo();
const canRedo = plugin.canRedo();
plugin.clearHistory();
```

### Compound Actions (Transactions)

When a user edit cascades programmatic changes to other fields, group them into
a single undo step so Ctrl+Z reverts everything at once:

```ts
grid.on('cell-commit', () => {
  const undoRedo = grid.getPluginByName('undoRedo');
  undoRedo.beginTransaction();

  // Record cascaded updates manually
  const oldTotal = row.total;
  undoRedo.recordEdit(rowIndex, 'total', oldTotal, newTotal);
  grid.updateRow(rowId, { total: newTotal });

  // End after the auto-recorded original edit is captured
  queueMicrotask(() => undoRedo.endTransaction());
});
```

| Method              | Description                                         |
| ------------------- | --------------------------------------------------- |
| `beginTransaction()`| Start buffering edits into a compound group         |
| `endTransaction()`  | Finalize and push the compound as a single undo step|
| `recordEdit()`      | Manually record a cell edit (buffered during txn)   |

## Events

```ts
// UndoRedoEventsDemo.astro
import '@toolbox-web/grid';
import { queryGrid } from '@toolbox-web/grid';
import '@toolbox-web/grid/features/editing';
import '@toolbox-web/grid/features/undo-redo';

const container = document.getElementById('undo-redo-events-demo');
if (container) {
  const grid = queryGrid('tbw-grid', container);

  grid.gridConfig = {
    columns: [
      { field: 'name', header: 'Name', editable: true },
      { field: 'department', header: 'Department', editable: true },
      { field: 'salary', header: 'Salary', type: 'number', editable: true },
    ],
    features: {
      editing: 'dblclick',
      undoRedo: true,
    },
  };

  grid.rows = [
    { name: 'Alice Johnson', department: 'Engineering', salary: 95000 },
    { name: 'Bob Smith', department: 'Marketing', salary: 75000 },
    { name: 'Carol White', department: 'Sales', salary: 68000 },
  ];

  const log = container.querySelector('#undo-redo-events-log');
  const clearBtn = container.querySelector('#clear-undo-redo-events-log');
  const undoBtn = container.querySelector('#undo-events-btn');
  const redoBtn = container.querySelector('#redo-events-btn');

  function addLog(type: string, detail: string) {
    if (!log) return;
    const msg = document.createElement('div');
    msg.innerHTML = `<span class="event-type">[${type}]</span> ${detail}`;
    log.insertBefore(msg, log.firstChild);
    while (log.children.length > 15) log.lastChild?.remove();
  }

  clearBtn?.addEventListener('click', () => { if (log) log.innerHTML = ''; });

  undoBtn?.addEventListener('click', () => {
    grid.getPluginByName('undoRedo')?.undo();
  });

  redoBtn?.addEventListener('click', () => {
    grid.getPluginByName('undoRedo')?.redo();
  });

  grid.on('undo', (d) => {
    addLog('undo', `field: ${d.field}, restored: "${d.oldValue}"`);
  });

  grid.on('redo', (d) => {
    addLog('redo', `field: ${d.field}, reapplied: "${d.value}"`);
  });
}
```

### undo / redo

Fired after an undo or redo action is applied:

```ts
grid.on('undo', ({ action, type }) => {
  console.log(`${type}: reverted ${action.field} on row ${action.rowIndex}`);
});

grid.on('redo', ({ action, type }) => {
  console.log(`${type}: reapplied ${action.field} on row ${action.rowIndex}`);
});
```

See [`UndoRedoDetail`](./Interfaces/UndoRedoDetail/) for the full event payload type.

## See Also

- **[Editing](/grid/plugins/editing.md)** — Inline cell editing (required)
- **[Clipboard](/grid/plugins/clipboard.md)** — Copy/paste cells
