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 {
  cellFlashArrowTemplate,
  CellFlashPlugin,
  ColumnStretchPlugin,
  EventManagerPlugin,
  HistoryPlugin,
  RowOddPlugin,
  RowSelectPlugin,
} from '@revolist/revogrid-pro';
import { currentTheme, useRandomData } from '../composables/useRandomData';
import { defineHistoryControls } from '@revolist/revogrid-pro';

const { createRandomData } = useRandomData();
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.theme = isDark() ? 'darkMaterial' : 'material';
  grid.stretch = 'all';
  grid.history = {
    undoStack: [],
    redoStack: [],
    maxStackSize: 200,
    disabled: false,
  };
  grid.source = createRandomData(100);
  grid.columns = [
    { name: 'ID', prop: 'id', rowSelect: true, size: 80 },
    { name: 'Fruit', prop: 'name' },
    {
      name: 'Price',
      prop: 'price',
      flash: () => true,
      cellTemplate: cellFlashArrowTemplate(),
    },
  ];
  grid.plugins = [
    EventManagerPlugin,
    HistoryPlugin,
    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);
}
React tsx
// src/components/history/History.tsx

import { useState, useMemo, useRef, useEffect } from 'react';
import { RevoGrid, type DataType } from '@revolist/react-datagrid';
import {
  cellFlashArrowTemplate,
  CellFlashPlugin,
  ColumnStretchPlugin,
  EventManagerPlugin,
  HistoryPlugin,
  RowOddPlugin,
  RowSelectPlugin,
  defineHistoryControls,
  type HistoryConfig,
} from '@revolist/revogrid-pro';
import { useRandomData, currentTheme } from '../composables/useRandomData';

function History() {
  const { isDark } = currentTheme();
  const { createRandomData } = useRandomData();
  const gridRef = useRef<HTMLRevoGridElement>(null);
  const toolbarRef = useRef<HTMLDivElement>(null);
  const [source] = useState<DataType[]>(() => createRandomData(100));
  const history = useMemo<HistoryConfig>(() => ({
    undoStack: [],
    redoStack: [],
    maxStackSize: 200,
    disabled: false,
  }), []);

  const columns = useMemo(() => [
    { name: 'ID', prop: 'id', rowSelect: true, size: 80 },
    { name: 'Fruit', prop: 'name' },
    { name: 'Price', prop: 'price', flash: () => true, cellTemplate: cellFlashArrowTemplate() },
  ], []);

  const plugins = useMemo(() => [
    EventManagerPlugin,
    HistoryPlugin,
    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}
        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"
      :history="history"
      :range="true"
      stretch="all"
      hide-attribution
    />
  </div>
</template>

<script setup lang="ts">
import { nextTick, onMounted, ref } from 'vue';
import { currentThemeVue, useRandomData } from '../composables/useRandomData';
import { VGrid } from '@revolist/vue3-datagrid';
import {
  cellFlashArrowTemplate,
  CellFlashPlugin,
  ColumnStretchPlugin,
  EventManagerPlugin,
  HistoryPlugin,
  RowOddPlugin,
  RowSelectPlugin,
  defineHistoryControls,
  type HistoryConfig,
} from '@revolist/revogrid-pro';

const { createRandomData } = useRandomData();
const { isDark } = currentThemeVue();

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

const columns = ref([
  { name: 'ID', prop: 'id', rowSelect: true, size: 80 },
  { name: 'Fruit', prop: 'name' },
  {
    name: 'Price',
    prop: 'price',
    flash: () => true,
    cellTemplate: cellFlashArrowTemplate(),
  },
]);

const plugins = ref([
  EventManagerPlugin,
  HistoryPlugin,
  ColumnStretchPlugin,
  CellFlashPlugin,
  RowOddPlugin,
  RowSelectPlugin,
]);

const rows = ref(createRandomData(100));
const history: HistoryConfig = {
  undoStack: [],
  redoStack: [],
  maxStackSize: 200,
  disabled: false,
};

onMounted(async () => {
  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);
});
</script>
Angular ts
import { Component, ViewChild, ElementRef, ViewEncapsulation, AfterViewInit, NO_ERRORS_SCHEMA } from '@angular/core';
import { RevoGrid } from '@revolist/angular-datagrid';
import {
  cellFlashArrowTemplate,
  CellFlashPlugin,
  ColumnStretchPlugin,
  EventManagerPlugin,
  HistoryPlugin,
  RowOddPlugin,
  RowSelectPlugin,
  defineHistoryControls,
  type HistoryConfig,
} from '@revolist/revogrid-pro';
import { currentTheme, useRandomData } from '../composables/useRandomData';

@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"
        [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 = useRandomData().createRandomData(100);
  history: HistoryConfig = {
    undoStack: [],
    redoStack: [],
    maxStackSize: 200,
    disabled: false,
  };

  columns = [
    { name: 'ID', prop: 'id', rowSelect: true, size: 80 },
    { name: 'Fruit', prop: 'name' },
    { name: 'Price', prop: 'price', flash: () => true, cellTemplate: cellFlashArrowTemplate() },
  ];

  plugins = [
    EventManagerPlugin,
    HistoryPlugin,
    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, ensuring optimal performance while retaining a comprehensive history of edits.
  • Custom Event Hooks: Supports custom behavior via beforeundo and beforeredo events, allowing full control over the undo/redo process.

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

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

To call undo(), redo(), clear(), or disable(), 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) {
history.undo();
}
}
  • Undo/redo does not record changes: ensure plugin order is [EventManagerPlugin, HistoryPlugin].
  • Calling plugin methods does not work: call getPlugins() first, then find HistoryPlugin.
  • 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.

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.providers.data.setRangeData(data, event.detail.type || lastChange.type);
this.redoStack.push(lastChange);
}
}
}

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.