Skip to content

History Undo/Redo

The History Plugin brings powerful undo and redo capabilities to your data grid, making it easier to manage user edits and maintain data integrity.


Source code
TypeScriptts
import { defineCustomElements } from '@revolist/revogrid/loader';
defineCustomElements();

import {
  AutoFillPlugin,
  CellFlashPlugin,
  ColumnStretchPlugin,
  EventManagerPlugin,
  HistoryPlugin,
  RowEditPlugin,
  RowOddPlugin,
  RowSelectPlugin,
} from '@revolist/revogrid-pro';
import { currentTheme } from '../composables/useRandomData';
import { defineHistoryControls } from '@revolist/revogrid-pro';
import {
  createHistoryConfig,
  createHistoryColumns,
  createHistoryRows,
  historyColumnTypes,
  historyEditors,
} from './history-demo-data';

const { isDark } = currentTheme();

export function load(parentSelector: string) {
  const parent = document.querySelector(parentSelector);
  if (!parent) return;

  const container = document.createElement('div');
  container.className = 'grow flex flex-col h-full';

  const grid = document.createElement('revo-grid') as HTMLRevoGridElement;
  grid.className = 'cell-border grow';
  grid.range = true;
  grid.hideAttribution = true;
  grid.columnTypes = historyColumnTypes;
  grid.editors = historyEditors;
  grid.eventManager = {
    applyEventsToSource: true,
  };
  grid.theme = isDark() ? 'darkMaterial' : 'material';
  grid.stretch = 'all';
  grid.history = createHistoryConfig();
  grid.columns = createHistoryColumns();
  grid.plugins = [
    EventManagerPlugin,
    HistoryPlugin,
    AutoFillPlugin,
    RowEditPlugin,
    ColumnStretchPlugin,
    CellFlashPlugin,
    RowOddPlugin,
    RowSelectPlugin,
  ];

  // Toolbar with undo/redo — created after grid is in DOM so getPlugins() works
  container.appendChild(grid);
  parent.appendChild(container);

  const toolbar = document.createElement('div');
  container.prepend(toolbar);
  defineHistoryControls(toolbar, grid);
  grid.source = createHistoryRows();

  return () => container.remove();
}
Reacttsx
// src/components/history/History.tsx

import { useState, useMemo, useRef, useEffect } from 'react';
import { RevoGrid, type DataType } from '@revolist/react-datagrid';
import {
  AutoFillPlugin,
  CellFlashPlugin,
  ColumnStretchPlugin,
  EventManagerPlugin,
  HistoryPlugin,
  RowEditPlugin,
  RowOddPlugin,
  RowSelectPlugin,
  defineHistoryControls,
} from '@revolist/revogrid-pro';
import { currentTheme } from '../composables/useRandomData';
import {
  createHistoryConfig,
  createHistoryColumns,
  createHistoryRows,
  historyColumnTypes,
  historyEditors,
} from './history-demo-data';

function History() {
  const { isDark } = currentTheme();
  const gridRef = useRef<HTMLRevoGridElement>(null);
  const toolbarRef = useRef<HTMLDivElement>(null);
  const [source] = useState<DataType[]>(() => createHistoryRows());
  const history = useMemo(() => createHistoryConfig(), []);

  const columns = useMemo(() => createHistoryColumns(), []);
  const columnTypes = useMemo(() => historyColumnTypes, []);
  const editors = useMemo(() => historyEditors, []);
  const eventManager = useMemo(() => ({ applyEventsToSource: true }), []);

  const plugins = useMemo(() => [
    EventManagerPlugin,
    HistoryPlugin,
    AutoFillPlugin,
    RowEditPlugin,
    ColumnStretchPlugin,
    CellFlashPlugin,
    RowOddPlugin,
    RowSelectPlugin,
  ], []);

  useEffect(() => {
    if (toolbarRef.current && gridRef.current) {
      defineHistoryControls(toolbarRef.current, gridRef.current);
    }
  }, []);

  return (
    <div className="grow flex flex-col h-full">
      <div ref={toolbarRef} />
      <RevoGrid
        ref={gridRef}
        className="cell-border grow"
        theme={isDark() ? 'darkMaterial' : 'material'}
        columns={columns}
        source={source}
        plugins={plugins}
        columnTypes={columnTypes}
        editors={editors}
        eventManager={eventManager}
        history={history}
        range
        stretch="all"
        hideAttribution
      />
    </div>
  );
}

export default History;
Vuevue
// src/components/history/History.vue

<template>
  <div class="grow flex flex-col h-full">
    <div ref="toolbarRef"></div>
    <VGrid
      class="cell-border grow"
      ref="gridRef"
      :theme="isDark ? 'darkMaterial' : 'material'"
      :columns="columns"
      :source="rows"
      :plugins="plugins"
      :columnTypes="columnTypes"
      :editors="editors"
      :event-manager.prop="eventManager"
      :history.prop="history"
      :cell-flash.prop="cellFlash"
      :range="true"
      @aftergridinit="initHistoryControls"
      stretch="all"
      hide-attribution
    />
  </div>
</template>

<script setup lang="ts">
import { nextTick, onMounted, ref } from 'vue';
import { currentThemeVue } from '../composables/useRandomData';
import { VGrid } from '@revolist/vue3-datagrid';
import {
  AutoFillPlugin,
  CellFlashPlugin,
  ColumnStretchPlugin,
  EventManagerPlugin,
  HistoryPlugin,
  RowEditPlugin,
  RowOddPlugin,
  RowSelectPlugin,
  defineHistoryControls,
} from '@revolist/revogrid-pro';
import {
  createHistoryConfig,
  createHistoryColumns,
  createHistoryRows,
  historyColumnTypes,
  historyEditors,
} from './history-demo-data';

const { isDark } = currentThemeVue();

const gridRef = ref<HTMLRevoGridElement | null>(null);
const toolbarRef = ref<HTMLElement | null>(null);

const columns = createHistoryColumns();
const columnTypes = historyColumnTypes;
const editors = historyEditors;
const eventManager = { applyEventsToSource: true };

const plugins = [
  EventManagerPlugin,
  HistoryPlugin,
  AutoFillPlugin,
  RowEditPlugin,
  ColumnStretchPlugin,
  CellFlashPlugin,
  RowOddPlugin,
  RowSelectPlugin,
];

const rows = ref(createHistoryRows());
const history = createHistoryConfig();
const cellFlash = {
  duration: 900,
  rowDuration: 1200,
  queue: 'merge',
  mode: 'cell',
  clearOnSourceChange: false,
  aria: true,
};

let controlsInitialized = false;

async function initializeHistoryControls() {
  if (controlsInitialized) {
    return;
  }

  await nextTick();

  const toolbar = toolbarRef.value;
  if (!toolbar) {
    return;
  }

  const grid = ((gridRef.value as any)?.$el ?? gridRef.value) as HTMLRevoGridElement | null;
  if (!grid || typeof grid.addEventListener !== 'function' || typeof grid.getPlugins !== 'function') {
    return;
  }

  defineHistoryControls(toolbar, grid);
  controlsInitialized = true;
}

onMounted(() => {
  void initializeHistoryControls();
});

const initHistoryControls = () => {
  void initializeHistoryControls();
};
</script>
Angularts
import { Component, ViewChild, ElementRef, ViewEncapsulation, AfterViewInit, NO_ERRORS_SCHEMA } from '@angular/core';
import { RevoGrid } from '@revolist/angular-datagrid';
import {
  AutoFillPlugin,
  CellFlashPlugin,
  ColumnStretchPlugin,
  EventManagerPlugin,
  HistoryPlugin,
  RowEditPlugin,
  RowOddPlugin,
  RowSelectPlugin,
  defineHistoryControls,
} from '@revolist/revogrid-pro';
import { currentTheme } from '../composables/useRandomData';
import {
  createHistoryConfig,
  createHistoryColumns,
  createHistoryRows,
  historyColumnTypes,
  historyEditors,
} from './history-demo-data';

@Component({
  selector: 'history-grid',
  standalone: true,
  imports: [RevoGrid],
  template: `
    <div class="grow flex flex-col h-full">
      <div #toolbar></div>
      <revo-grid
        #grid
        class="cell-border grow"
        [theme]="theme"
        [columns]="columns"
        [source]="rows"
        [plugins]="plugins"
        [columnTypes]="columnTypes"
        [editors]="editors"
        [eventManager]="eventManager"
        [history]="history"
        [range]="true"
        stretch="all"
        [hideAttribution]="true"
      ></revo-grid>
    </div>
  `,
  encapsulation: ViewEncapsulation.None,
  // Allows Angular demos to bind RevoGrid plugin props that are not wrapper inputs.
  schemas: [NO_ERRORS_SCHEMA],
})
export class HistoryGridComponent implements AfterViewInit {
  @ViewChild('grid', { read: ElementRef }) gridElement!: ElementRef;
  @ViewChild('toolbar', { read: ElementRef }) toolbarElement!: ElementRef;

  theme = currentTheme().isDark() ? 'darkMaterial' : 'material';
  rows = createHistoryRows();
  history = createHistoryConfig();
  columnTypes = historyColumnTypes;
  editors = historyEditors;
  eventManager = { applyEventsToSource: true };

  columns = createHistoryColumns();

  plugins = [
    EventManagerPlugin,
    HistoryPlugin,
    AutoFillPlugin,
    RowEditPlugin,
    ColumnStretchPlugin,
    CellFlashPlugin,
    RowOddPlugin,
    RowSelectPlugin,
  ];

  ngAfterViewInit() {
    defineHistoryControls(this.toolbarElement.nativeElement, this.gridElement.nativeElement);
  }
}

Key Features

  • Undo/Redo Management: Automatically tracks user changes with the ability to undo/redo actions using keyboard shortcuts.
  • Configurable Stack Size: Tracks up to 200 changes by default, and accepts partial history config when you only need to override one option.
  • Provider Persistence: Can load and save stack state through local cache and custom server providers for same-dataset sessions.
  • Custom Event Hooks: Supports custom behavior via beforeundo and beforeredo events, allowing full control over the undo/redo process.
  • Pro Editing Coverage: Tracks regular cell edits, Pro editor templates, textarea editors, range edits, clipboard paste, autofill, row-edit saves, and audit restore replays. HistoryPlugin installs its Event Manager dependency automatically.
  • Context-menu Transactions: Tracks each built-in Cut, Paste, Clear, row mutation, or column-schema mutation as one chronological undo/redo action. Copy remains non-mutating and is not recorded.

Register HistoryPlugin to enable undo/redo. It automatically installs EventManagerPlugin when an Event Manager instance is not already present, so normal edit events enter the undo stack without a second undocumented plugin.

import {
HistoryPlugin,
createLocalStorageHistoryAdapter,
} from '@revolist/revogrid-pro';
plugins = [HistoryPlugin];

Undo, redo, and public replay operations emit beforehistedit. The Event Manager installed by History handles that event and emits the normal gridedit flow. If your application already registers EventManagerPlugin, History reuses that instance rather than creating a duplicate.

AuditHistoryPlugin automatically reuses or installs HistoryPlugin for restore replay, keyboard shortcuts, and existing History integrations. Keep History configuration on grid.history; Audit History does not define a nested History config.

When HistoryPlugin and DataGridContextMenuPlugin are installed together, each built-in mutating command is atomic: a multi-cell Cut, Paste, cell Clear, or row Clear creates one history entry, and undo restores all affected cells in reverse edit order. Redo reapplies them in forward order.

History also snapshots the affected authored structure for these commands:

  • Rows: insert above/below, duplicate, delete, pin top/bottom, and unpin.
  • Columns: insert left/right, duplicate, delete, pin left/right, unpin, and move left/right/start/end.

Plain Copy and Copy with headers do not create entries because they do not mutate the grid. A commandHandlers override is application-owned and is also excluded: if a custom handler changes remote or application state, record and restore that state in the application instead. Canceled, unavailable, and no-op commands leave both stacks unchanged.

In-memory structural entries retain row object identity, nested column groups, renderers, editors, and other function-valued column fields. Persisted entries should use serializable row and column definitions; storage formats such as JSON cannot restore functions. Context-menu replay temporarily bypasses clearOnSourceChange only for its own row-source replacements. Unrelated application source replacements continue to use the configured clearing policy.

grid.history is optional. Add it only when you need custom stacks, a smaller or larger stack limit, or an initially disabled plugin.

grid.history = {
maxStackSize: 100,
keyboardShortcutsWithoutFocus: false,
};

Undo and redo stacks are index-based, so persisted history is safe only when the user returns to the same dataset, row order, and compatible column schema. Structural context-menu entries additionally require serializable row and column definitions. For long-term recovery across changing data, use AuditHistoryPlugin, which restores by stable row identity.

Use sourceId to reject stale server states. With multiple providers, the plugin loads the first provider immediately, then applies later providers only when their updatedAt state is newer and their sourceId is compatible.

When storage is configured, clearOnSourceChange defaults to false so the initial data load does not erase the restored stack. Set it to true only when your app wants every source replacement to clear persisted undo/redo state.

grid.history = {
sourceId: `invoices:${workspaceId}:${datasetVersion}`,
storageProviders: [
createLocalStorageHistoryAdapter('invoices:history'),
{
async load() {
const response = await fetch('/api/grid-history');
return response.json();
},
async save(state) {
await fetch('/api/grid-history', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(state),
});
},
async clear() {
await fetch('/api/grid-history', { method: 'DELETE' });
},
},
],
};

Listen to historyerror when you want to surface provider failures or skipped states:

grid.addEventListener('historyerror', (event) => {
console.warn(event.detail.phase, event.detail.error);
});

To call undo(), redo(), clear(), disable(), or state helpers, get plugin instances from the grid and find the HistoryPlugin instance.

async undo() {
// get plugins from the grid
const plugins = await this.gridRefNativeElement.getPlugins();
const history = plugins.find((plugin) => plugin instanceof HistoryPlugin);
if (history) {
if (history.canUndo()) {
history.undo();
}
}
}

For custom controls, listen to historychanged instead of reading plugin internals. The event detail includes undoStackSize, redoStackSize, canUndo, canRedo, and disabled.

When HistoryPlugin and RowOrderPlugin are installed together, every successful row drop is one undo/redo action by default. This includes selected row blocks and grouped drops that update group fields.

grid.history = {
rowOrderHistory: 'track', // default
};

Use rowOrderHistory: 'clear' to discard existing history after a committed drop, or rowOrderHistory: 'preserve' when another integration records a richer domain snapshot. Gantt uses preserve and records canonical tasks and dependencies together.

  • Undo/redo does not record changes: ensure HistoryPlugin is present in the grid’s plugins array. Event Manager setup and keyboard shortcuts are automatic.
  • Restore flashes are missing in Audit History: install EventManagerPlugin and CellFlashPlugin; Audit restore replay flows through beforehistedit and the normal edit event path.
  • Calling plugin methods does not work: call getPlugins() first, then find HistoryPlugin.
  • History clears after sort, filter, or source replacement: this is intentional because those operations can change visible row indexes. Successful row reorders are tracked unless rowOrderHistory is clear or preserve.
  • Persisted history was skipped: check the historyerror event for a storage-source-mismatch phase and update the configured sourceId.
  • Need a known-good reference: see code in: revogrid-pro/src/components/history/**.**.

The History Plugin provides four key methods to enhance control over undo/redo functionality:

  • clear(): Resets both the undo and redo stacks, clearing all recorded changes.
  • disable(disable = true): Temporarily disables the plugin, preventing changes from being recorded or undo/redo operations from being triggered.
  • undo(): Reverts the last change recorded in the undo stack and moves it to the redo stack for potential reapplication.
  • redo(): Reapplies the most recent change from the redo stack, moving it back to the undo stack for further management.
  • canUndo() / canRedo(): Returns whether undo or redo is currently available.
  • getUndoStackSize() / getRedoStackSize(): Returns stack counts for custom controls.
  • isDisabled(): Returns whether history is disabled.

These methods give developers flexibility in managing user actions and tailoring the plugin’s behavior to specific application requirements.

The plugin listens to edit events (onEditEvent) and tracks changes in undo and redo stacks. Keyboard shortcuts like Ctrl+Z for undo and Ctrl+Y (or Ctrl+Shift+Z) for redo are supported out of the box. You can customize its behavior by using the BEFORE_UNDO_EVENT and BEFORE_REDO_EVENT hooks.

Here’s a quick snippet showing how the plugin processes undo actions:

undo() {
if (this.undoStack.length === 0) return;
const lastChange = this.undoStack.pop();
if (lastChange) {
const event = this.emit(BEFORE_UNDO_EVENT, {
data: lastChange.previousData,
type: lastChange.type,
lastChange,
});
if (!event.defaultPrevented) {
const data = event.detail.data || lastChange.previousData;
this.replayChange({
data,
type: event.detail.type || lastChange.type,
models: lastChange.models,
previousData: lastChange.data,
});
this.redoStack.push(lastChange);
}
}
}

replayChange() emits beforehistedit first. If no integration handles that event, History falls back to providers.data.setRangeData(...).

Try it Out!

Add the History Plugin to your RevoGrid instance and experience seamless undo/redo functionality. This plugin is especially useful in applications that handle complex data operations, ensuring users can easily revert or reapply changes as needed.