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
TypeScript ts
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();
}
React tsx
// 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;
Vue vue
// 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>
Angular ts
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 when paired with EventManagerPlugin.

Use EventManagerPlugin together with HistoryPlugin. HistoryPlugin listens to ON_EDIT_EVENT, which is emitted by EventManagerPlugin.

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

Undo, redo, and public replay operations emit beforehistedit. When EventManagerPlugin is installed, it handles that event and emits the normal gridedit flow. When it is not installed, History applies the replay directly with setRangeData.

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.

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 and row order. 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.

  • Undo/redo does not record changes: ensure plugin order is [EventManagerPlugin, HistoryPlugin].
  • 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, row-order, or source replacement: this is intentional because those operations can change visible row indexes.
  • 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.