History Tracker

The Jspreadsheet History Tracker automatically records all changes to the spreadsheet and provides undo/redo functionality through Ctrl+Z and Ctrl+Y keyboard shortcuts. Developers can monitor interactions through historical events and manage the history stack programmatically, including options to ignore specific operations or cascade multiple actions into a single history entry.

What's new with Version 13

  • Collaboration-aware undo: history records follow rows, columns and worksheets by identity. Undo lands on the right cells after inserts, deletes or moves made locally or by other connected users, relocates targets deleted and later restored, and skips targets that no longer exist instead of writing to the wrong cell
  • One entry per operation: a paste with its cut and merges, a nested header move or a multi column sort apply and revert as a unit
  • Personal view state: zoom, group open and close, search terms and the current page are never recorded in the history
  • Performance: recording is constant-time per edit, so long sessions no longer degrade

Documentation

Methods

The following methods provide programmatic control over the history tracker. The undo() and redo() methods are automatically bound to Ctrl+Z and Ctrl+Y keyboard shortcuts:

Method Description
history.undo Undo the last spreadsheet changes.
jspreadsheet.history.undo() : void
history.redo Redo the most recent spreadsheet changes.
jspreadsheet.history.redo() : void
history.reset Clear all history entries.
jspreadsheet.history.reset() : void

Advanced usage

Method Description
history(object) Add a new entry to the history tracker.
jspreadsheet.history(changes: Object) : void

Events

The following events are triggered during history operations:

Event Description
onredo onredo(worksheet: Object, info: Object) : null
Triggered after a redo operation is performed.
onundo onundo(worksheet: Object, info: Object) : null
Triggered after an undo operation is performed.

Interface

The history tracker interface provides the following properties and methods:

Property Description
index: number Current position in the history stack
actions: [] Array of history items
cascade: boolean When true, the next action is cascaded into the same existing history element
ignore: boolean When true, no history will be added to the tracker
progress: string Indicates if a history process is ongoing
undo: () => void Undo the last action
redo: () => void Redo the most recent action
reset: () => void Clear all history entries

Undo in collaborative sessions

History records reference worksheets by id and rows and columns by identity, not by position. When another connected user inserts, deletes or moves rows or columns, a local undo still applies to the cells it was recorded for, at their current position. A target deleted by another user and later restored is relocated through its identity; a target that no longer exists is skipped, and the rest of the same record still applies. This covers values, formulas, styles, comments, merges, visibility, widths, heights, headers, groups and filters. Remote operations replayed through the Jspreadsheet Server never enter the local history.

Examples

Programmatic History Control

This example demonstrates how to control undo/redo operations programmatically using the global jspreadsheet.history object.

<html>
<script src="https://jspreadsheet.com/v13/jspreadsheet.js"></script>
<script src="https://jsuites.net/v6/jsuites.js"></script>
<link rel="stylesheet" href="https://jspreadsheet.com/v13/jspreadsheet.css" type="text/css" />
<link rel="stylesheet" href="https://jsuites.net/v6/jsuites.css" type="text/css" />

<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Material+Icons" />

<div id='spreadsheet'></div>

<p><input type="button" value="Undo" id="btn1" />
<input type="button" value="Redo" id="btn2" />
<input type="button" value="Reset" id="btn3" /></p>

<script>
// You can use the following license for quick testing on localhost, StackBlitz, or CodeSandbox.
// The license is valid for one day, after which the spreadsheet will become read-only.
// For a longer trial period, you can create a free account and generate a demo license with an extended expiration date.
jspreadsheet.setLicense('MGQ2MmIzN2VlMGM2ODRiMDY1OTc3MGRiMzY4OTJhMzk5ZjY4MmQyMzUxZmE2MzRhMzVlZjE5ZDM1NzE5NTZhNzIwNzU5MjgzMjg5NWU3YmNjZTNiNzMwM2I1OWEyZDVmODkwYjUwNjQ4MmVkYzM4MDM0NmRhMGUzZDU3OTI5MWYsZXlKamJHbGxiblJKWkNJNklpSXNJbTVoYldVaU9pSktjM0J5WldGa2MyaGxaWFFpTENKa1lYUmxJam94TnpnNU16ZzJPRFF5TENKa2IyMWhhVzRpT2xzaWFuTndjbVZoWkhOb1pXVjBMbU52YlNJc0ltTnZaR1Z6WVc1a1ltOTRMbWx2SWl3aWFuTm9aV3hzTG01bGRDSXNJbU56WWk1aGNIQWlMQ0p6ZEdGamEySnNhWFI2TG1sdklpd2lkMlZpWTI5dWRHRnBibVZ5TG1sdklpd2liRzlqWVd4b2IzTjBJbDBzSW5Cc1lXNGlPaUl6TkNJc0luTmpiM0JsSWpwYkluWTNJaXdpZGpnaUxDSjJPU0lzSW5ZeE1DSXNJbll4TVNJc0luWXhNaUlzSW5ZeE15SXNJbU5vWVhKMGN5SXNJbVp2Y20xeklpd2labTl5YlhWc1lTSXNJbkJoY25ObGNpSXNJbkpsYm1SbGNpSXNJbU52YlcxbGJuUnpJaXdpYVcxd2IzSjBaWElpTENKaVlYSWlMQ0oyWVd4cFpHRjBhVzl1Y3lJc0luTmxZWEpqYUNJc0luQnlhVzUwSWl3aWMyaGxaWFJ6SWl3aVkyeHBaVzUwSWl3aWMyVnlkbVZ5SWl3aWMyaGhjR1Z6SWl3aVptOXliV0YwSWl3aWNHbDJiM1FpWFN3aVpHVnRieUk2ZEhKMVpYMD0=');

// Create the spreadsheet
let spreadsheet = jspreadsheet(document.getElementById('spreadsheet'), {
    worksheets: [{
        minDimensions: [6, 6],
        data: [
            ['Sample', 'Data', 'Here'],
            ['Row 2', 'Value', '123'],
            ['Test', 'More', 'Data']
        ]
    }],
    onundo: function(worksheet, info) {
        console.log('Undo action:', info);
    },
    onredo: function(worksheet, info) {
        console.log('Redo action:', info);
    }
});

document.getElementById("btn1").onclick = () => {
    console.log('History index before undo:', jspreadsheet.history.index);
    jspreadsheet.history.undo();
}
document.getElementById("btn2").onclick = () => {
    console.log('History index before redo:', jspreadsheet.history.index);
    jspreadsheet.history.redo();
}
document.getElementById("btn3").onclick = () => {
    console.log('Clearing', jspreadsheet.history.actions.length, 'history items');
    jspreadsheet.history.reset();
}
</script>
</html>
import React, {useRef} from "react";
import { Spreadsheet, Worksheet, jspreadsheet } from "@jspreadsheet/react";
import "jsuites/dist/jsuites.css";
import "jspreadsheet/dist/jspreadsheet.css";

// You can use the following license for quick testing on localhost, StackBlitz, or CodeSandbox.
// The license is valid for one day, after which the spreadsheet will become read-only.
// For a longer trial period, you can create a free account and generate a demo license with an extended expiration date.
jspreadsheet.setLicense('MGQ2MmIzN2VlMGM2ODRiMDY1OTc3MGRiMzY4OTJhMzk5ZjY4MmQyMzUxZmE2MzRhMzVlZjE5ZDM1NzE5NTZhNzIwNzU5MjgzMjg5NWU3YmNjZTNiNzMwM2I1OWEyZDVmODkwYjUwNjQ4MmVkYzM4MDM0NmRhMGUzZDU3OTI5MWYsZXlKamJHbGxiblJKWkNJNklpSXNJbTVoYldVaU9pSktjM0J5WldGa2MyaGxaWFFpTENKa1lYUmxJam94TnpnNU16ZzJPRFF5TENKa2IyMWhhVzRpT2xzaWFuTndjbVZoWkhOb1pXVjBMbU52YlNJc0ltTnZaR1Z6WVc1a1ltOTRMbWx2SWl3aWFuTm9aV3hzTG01bGRDSXNJbU56WWk1aGNIQWlMQ0p6ZEdGamEySnNhWFI2TG1sdklpd2lkMlZpWTI5dWRHRnBibVZ5TG1sdklpd2liRzlqWVd4b2IzTjBJbDBzSW5Cc1lXNGlPaUl6TkNJc0luTmpiM0JsSWpwYkluWTNJaXdpZGpnaUxDSjJPU0lzSW5ZeE1DSXNJbll4TVNJc0luWXhNaUlzSW5ZeE15SXNJbU5vWVhKMGN5SXNJbVp2Y20xeklpd2labTl5YlhWc1lTSXNJbkJoY25ObGNpSXNJbkpsYm1SbGNpSXNJbU52YlcxbGJuUnpJaXdpYVcxd2IzSjBaWElpTENKaVlYSWlMQ0oyWVd4cFpHRjBhVzl1Y3lJc0luTmxZWEpqYUNJc0luQnlhVzUwSWl3aWMyaGxaWFJ6SWl3aVkyeHBaVzUwSWl3aWMyVnlkbVZ5SWl3aWMyaGhjR1Z6SWl3aVptOXliV0YwSWl3aWNHbDJiM1FpWFN3aVpHVnRieUk2ZEhKMVpYMD0=');

export default function App() {
    // Spreadsheet array of worksheets
    const spreadsheet = useRef();

    const handleUndo = () => {
        console.log('History before undo:', jspreadsheet.history.index);
        jspreadsheet.history.undo();
    };

    const handleRedo = () => {
        console.log('History before redo:', jspreadsheet.history.index);
        jspreadsheet.history.redo();
    };

    const handleReset = () => {
        console.log('Clearing', jspreadsheet.history.actions.length, 'history items');
        jspreadsheet.history.reset();
    };

    const onundo = (worksheet, info) => {
        console.log('Undo event:', info);
    };

    const onredo = (worksheet, info) => {
        console.log('Redo event:', info);
    };

    // Render component
    return (
        <>
            <Spreadsheet ref={spreadsheet} onundo={onundo} onredo={onredo}>
                <Worksheet
                    minDimensions={[8, 8]}
                    data={[['Sample', 'Data', 'Here'], ['Row 2', 'Value', '123']]}
                />
            </Spreadsheet>
            <input type="button" value="Undo" onClick={handleUndo}/>
            <input type="button" value="Redo" onClick={handleRedo}/>
            <input type="button" value="Reset" onClick={handleReset}/>
        </>
    );
}
<template>
    <Spreadsheet ref="spreadsheet" :onundo="onundo" :onredo="onredo">
        <Worksheet :minDimensions="[8,8]" :data="data" />
    </Spreadsheet>
    <input type="button" value="Undo" @click="undo" />
    <input type="button" value="Redo" @click="redo" />
    <input type="button" value="Reset" @click="reset" />
</template>

<script>
import { Spreadsheet, Worksheet, jspreadsheet } from "@jspreadsheet/vue";
import "jsuites/dist/jsuites.css";
import "jspreadsheet/dist/jspreadsheet.css";

// You can use the following license for quick testing on localhost, StackBlitz, or CodeSandbox.
// The license is valid for one day, after which the spreadsheet will become read-only.
// For a longer trial period, you can create a free account and generate a demo license with an extended expiration date.
jspreadsheet.setLicense('MGQ2MmIzN2VlMGM2ODRiMDY1OTc3MGRiMzY4OTJhMzk5ZjY4MmQyMzUxZmE2MzRhMzVlZjE5ZDM1NzE5NTZhNzIwNzU5MjgzMjg5NWU3YmNjZTNiNzMwM2I1OWEyZDVmODkwYjUwNjQ4MmVkYzM4MDM0NmRhMGUzZDU3OTI5MWYsZXlKamJHbGxiblJKWkNJNklpSXNJbTVoYldVaU9pSktjM0J5WldGa2MyaGxaWFFpTENKa1lYUmxJam94TnpnNU16ZzJPRFF5TENKa2IyMWhhVzRpT2xzaWFuTndjbVZoWkhOb1pXVjBMbU52YlNJc0ltTnZaR1Z6WVc1a1ltOTRMbWx2SWl3aWFuTm9aV3hzTG01bGRDSXNJbU56WWk1aGNIQWlMQ0p6ZEdGamEySnNhWFI2TG1sdklpd2lkMlZpWTI5dWRHRnBibVZ5TG1sdklpd2liRzlqWVd4b2IzTjBJbDBzSW5Cc1lXNGlPaUl6TkNJc0luTmpiM0JsSWpwYkluWTNJaXdpZGpnaUxDSjJPU0lzSW5ZeE1DSXNJbll4TVNJc0luWXhNaUlzSW5ZeE15SXNJbU5vWVhKMGN5SXNJbVp2Y20xeklpd2labTl5YlhWc1lTSXNJbkJoY25ObGNpSXNJbkpsYm1SbGNpSXNJbU52YlcxbGJuUnpJaXdpYVcxd2IzSjBaWElpTENKaVlYSWlMQ0oyWVd4cFpHRjBhVzl1Y3lJc0luTmxZWEpqYUNJc0luQnlhVzUwSWl3aWMyaGxaWFJ6SWl3aVkyeHBaVzUwSWl3aWMyVnlkbVZ5SWl3aWMyaGhjR1Z6SWl3aVptOXliV0YwSWl3aWNHbDJiM1FpWFN3aVpHVnRieUk2ZEhKMVpYMD0=');

export default {
    components: {
        Spreadsheet,
        Worksheet,
    },
    methods: {
        undo() {
            console.log('History before undo:', jspreadsheet.history.index);
            jspreadsheet.history.undo();
        },
        redo() {
            console.log('History before redo:', jspreadsheet.history.index);
            jspreadsheet.history.redo();
        },
        reset() {
            console.log('Clearing', jspreadsheet.history.actions.length, 'history items');
            jspreadsheet.history.reset();
        },
        onundo(worksheet, info) {
            console.log('Undo event:', info);
        },
        onredo(worksheet, info) {
            console.log('Redo event:', info);
        }
    },
    data() {
        return {
            data: [['Sample', 'Data', 'Here'], ['Row 2', 'Value', '123']]
        }
    }
}
</script>
import { Component, ViewChild, ElementRef } from '@angular/core';
import jspreadsheet from 'jspreadsheet';
import "jsuites/dist/jsuites.css";
import "jspreadsheet/dist/jspreadsheet.css";

// You can use the following license for quick testing on localhost, StackBlitz, or CodeSandbox.
// The license is valid for one day, after which the spreadsheet will become read-only.
// For a longer trial period, you can create a free account and generate a demo license with an extended expiration date.
jspreadsheet.setLicense('MGQ2MmIzN2VlMGM2ODRiMDY1OTc3MGRiMzY4OTJhMzk5ZjY4MmQyMzUxZmE2MzRhMzVlZjE5ZDM1NzE5NTZhNzIwNzU5MjgzMjg5NWU3YmNjZTNiNzMwM2I1OWEyZDVmODkwYjUwNjQ4MmVkYzM4MDM0NmRhMGUzZDU3OTI5MWYsZXlKamJHbGxiblJKWkNJNklpSXNJbTVoYldVaU9pSktjM0J5WldGa2MyaGxaWFFpTENKa1lYUmxJam94TnpnNU16ZzJPRFF5TENKa2IyMWhhVzRpT2xzaWFuTndjbVZoWkhOb1pXVjBMbU52YlNJc0ltTnZaR1Z6WVc1a1ltOTRMbWx2SWl3aWFuTm9aV3hzTG01bGRDSXNJbU56WWk1aGNIQWlMQ0p6ZEdGamEySnNhWFI2TG1sdklpd2lkMlZpWTI5dWRHRnBibVZ5TG1sdklpd2liRzlqWVd4b2IzTjBJbDBzSW5Cc1lXNGlPaUl6TkNJc0luTmpiM0JsSWpwYkluWTNJaXdpZGpnaUxDSjJPU0lzSW5ZeE1DSXNJbll4TVNJc0luWXhNaUlzSW5ZeE15SXNJbU5vWVhKMGN5SXNJbVp2Y20xeklpd2labTl5YlhWc1lTSXNJbkJoY25ObGNpSXNJbkpsYm1SbGNpSXNJbU52YlcxbGJuUnpJaXdpYVcxd2IzSjBaWElpTENKaVlYSWlMQ0oyWVd4cFpHRjBhVzl1Y3lJc0luTmxZWEpqYUNJc0luQnlhVzUwSWl3aWMyaGxaWFJ6SWl3aVkyeHBaVzUwSWl3aWMyVnlkbVZ5SWl3aWMyaGhjR1Z6SWl3aVptOXliV0YwSWl3aWNHbDJiM1FpWFN3aVpHVnRieUk2ZEhKMVpYMD0=');

// Create component
@Component({
  standalone: true,
  selector: 'app-root',
  template: `<div #spreadsheet></div>
        <input type="button" value="Undo" (click)="undo()" />
        <input type="button" value="Redo" (click)="redo()" />
        <input type="button" value="Reset" (click)="reset()" />`,
})
export class AppComponent {
  @ViewChild('spreadsheet') spreadsheet: ElementRef;
  // Worksheets
  worksheets: jspreadsheet.worksheetInstance[];
  // Create a new data grid
  ngAfterViewInit() {
    // Create spreadsheet
    this.worksheets = jspreadsheet(this.spreadsheet.nativeElement, {
      worksheets: [
        {
          minDimensions: [8, 8],
          data: [
            ['Sample', 'Data', 'Here'],
            ['Row 2', 'Value', '123'],
            ['Test', 'More', 'Data']
          ]
        },
      ],
      onundo: (worksheet, info) => {
        console.log('Undo event:', info);
      },
      onredo: (worksheet, info) => {
        console.log('Redo event:', info);
      }
    });
  }
  undo(): void {
    console.log('History before undo:', jspreadsheet.history.index);
    jspreadsheet.history.undo();
  }

  redo(): void {
    console.log('History before redo:', jspreadsheet.history.index);
    jspreadsheet.history.redo();
  }

  reset(): void {
    console.log('Clearing', jspreadsheet.history.actions.length, 'history items');
    jspreadsheet.history.reset();
  }
}

Ignoring History for Specific Operations

This example shows how to make changes without adding them to the history stack using the ignore flag.

<html>
<script src="https://jspreadsheet.com/v13/jspreadsheet.js"></script>
<script src="https://jsuites.net/v6/jsuites.js"></script>
<link rel="stylesheet" href="https://jspreadsheet.com/v13/jspreadsheet.css" type="text/css" />
<link rel="stylesheet" href="https://jsuites.net/v6/jsuites.css" type="text/css" />

<div id='spreadsheet'></div>

<p><input type="button" value="Set A1 (untracked)" id="btn1" />
<input type="button" value="Undo" id="btn2" /></p>

<script>
// You can use the following license for quick testing on localhost, StackBlitz, or CodeSandbox.
// The license is valid for one day, after which the spreadsheet will become read-only.
// For a longer trial period, you can create a free account and generate a demo license with an extended expiration date.
jspreadsheet.setLicense('MGQ2MmIzN2VlMGM2ODRiMDY1OTc3MGRiMzY4OTJhMzk5ZjY4MmQyMzUxZmE2MzRhMzVlZjE5ZDM1NzE5NTZhNzIwNzU5MjgzMjg5NWU3YmNjZTNiNzMwM2I1OWEyZDVmODkwYjUwNjQ4MmVkYzM4MDM0NmRhMGUzZDU3OTI5MWYsZXlKamJHbGxiblJKWkNJNklpSXNJbTVoYldVaU9pSktjM0J5WldGa2MyaGxaWFFpTENKa1lYUmxJam94TnpnNU16ZzJPRFF5TENKa2IyMWhhVzRpT2xzaWFuTndjbVZoWkhOb1pXVjBMbU52YlNJc0ltTnZaR1Z6WVc1a1ltOTRMbWx2SWl3aWFuTm9aV3hzTG01bGRDSXNJbU56WWk1aGNIQWlMQ0p6ZEdGamEySnNhWFI2TG1sdklpd2lkMlZpWTI5dWRHRnBibVZ5TG1sdklpd2liRzlqWVd4b2IzTjBJbDBzSW5Cc1lXNGlPaUl6TkNJc0luTmpiM0JsSWpwYkluWTNJaXdpZGpnaUxDSjJPU0lzSW5ZeE1DSXNJbll4TVNJc0luWXhNaUlzSW5ZeE15SXNJbU5vWVhKMGN5SXNJbVp2Y20xeklpd2labTl5YlhWc1lTSXNJbkJoY25ObGNpSXNJbkpsYm1SbGNpSXNJbU52YlcxbGJuUnpJaXdpYVcxd2IzSjBaWElpTENKaVlYSWlMQ0oyWVd4cFpHRjBhVzl1Y3lJc0luTmxZWEpqYUNJc0luQnlhVzUwSWl3aWMyaGxaWFJ6SWl3aVkyeHBaVzUwSWl3aWMyVnlkbVZ5SWl3aWMyaGhjR1Z6SWl3aVptOXliV0YwSWl3aWNHbDJiM1FpWFN3aVpHVnRieUk2ZEhKMVpYMD0=');

// Create the spreadsheet
let spreadsheet = jspreadsheet(document.getElementById('spreadsheet'), {
    worksheets: [{
        minDimensions: [6, 6],
    }],
});

document.getElementById("btn1").onclick = () => {
    // Ignored operation - will not be tracked in history
    jspreadsheet.history.ignore = true;
    spreadsheet[0].setValue('A1', Math.random()*1000);
    jspreadsheet.history.ignore = false;
}

document.getElementById("btn2").onclick = () => {
    jspreadsheet.history.undo();
}
</script>
</html>
import React, {useRef} from "react";
import { Spreadsheet, Worksheet, jspreadsheet } from "@jspreadsheet/react";
import "jsuites/dist/jsuites.css";
import "jspreadsheet/dist/jspreadsheet.css";

// You can use the following license for quick testing on localhost, StackBlitz, or CodeSandbox.
// The license is valid for one day, after which the spreadsheet will become read-only.
// For a longer trial period, you can create a free account and generate a demo license with an extended expiration date.
jspreadsheet.setLicense('MGQ2MmIzN2VlMGM2ODRiMDY1OTc3MGRiMzY4OTJhMzk5ZjY4MmQyMzUxZmE2MzRhMzVlZjE5ZDM1NzE5NTZhNzIwNzU5MjgzMjg5NWU3YmNjZTNiNzMwM2I1OWEyZDVmODkwYjUwNjQ4MmVkYzM4MDM0NmRhMGUzZDU3OTI5MWYsZXlKamJHbGxiblJKWkNJNklpSXNJbTVoYldVaU9pSktjM0J5WldGa2MyaGxaWFFpTENKa1lYUmxJam94TnpnNU16ZzJPRFF5TENKa2IyMWhhVzRpT2xzaWFuTndjbVZoWkhOb1pXVjBMbU52YlNJc0ltTnZaR1Z6WVc1a1ltOTRMbWx2SWl3aWFuTm9aV3hzTG01bGRDSXNJbU56WWk1aGNIQWlMQ0p6ZEdGamEySnNhWFI2TG1sdklpd2lkMlZpWTI5dWRHRnBibVZ5TG1sdklpd2liRzlqWVd4b2IzTjBJbDBzSW5Cc1lXNGlPaUl6TkNJc0luTmpiM0JsSWpwYkluWTNJaXdpZGpnaUxDSjJPU0lzSW5ZeE1DSXNJbll4TVNJc0luWXhNaUlzSW5ZeE15SXNJbU5vWVhKMGN5SXNJbVp2Y20xeklpd2labTl5YlhWc1lTSXNJbkJoY25ObGNpSXNJbkpsYm1SbGNpSXNJbU52YlcxbGJuUnpJaXdpYVcxd2IzSjBaWElpTENKaVlYSWlMQ0oyWVd4cFpHRjBhVzl1Y3lJc0luTmxZWEpqYUNJc0luQnlhVzUwSWl3aWMyaGxaWFJ6SWl3aVkyeHBaVzUwSWl3aWMyVnlkbVZ5SWl3aWMyaGhjR1Z6SWl3aVptOXliV0YwSWl3aWNHbDJiM1FpWFN3aVpHVnRieUk2ZEhKMVpYMD0=');

export default function App() {
    const spreadsheet = useRef();

    const setUntrackedValue = () => {
        if (spreadsheet.current) {
            jspreadsheet.history.ignore = true;
            spreadsheet.current[0].setValue('A1', Math.random() * 1000);
            jspreadsheet.history.ignore = false;
        }
    };

    const handleUndo = () => {
        jspreadsheet.history.undo();
    };

    return (
        <>
            <Spreadsheet ref={spreadsheet}>
                <Worksheet minDimensions={[6, 6]} />
            </Spreadsheet>
            <input type="button" value="Set A1 (untracked)" onClick={setUntrackedValue}/>
            <input type="button" value="Undo" onClick={handleUndo}/>
        </>
    );
}
<template>
    <Spreadsheet ref="spreadsheet">
        <Worksheet :minDimensions="[6,6]" />
    </Spreadsheet>
    <input type="button" value="Set A1 (untracked)" @click="setUntrackedValue" />
    <input type="button" value="Undo" @click="undo" />
</template>

<script>
import { Spreadsheet, Worksheet, jspreadsheet } from "@jspreadsheet/vue";
import "jsuites/dist/jsuites.css";
import "jspreadsheet/dist/jspreadsheet.css";

// You can use the following license for quick testing on localhost, StackBlitz, or CodeSandbox.
// The license is valid for one day, after which the spreadsheet will become read-only.
// For a longer trial period, you can create a free account and generate a demo license with an extended expiration date.
jspreadsheet.setLicense('MGQ2MmIzN2VlMGM2ODRiMDY1OTc3MGRiMzY4OTJhMzk5ZjY4MmQyMzUxZmE2MzRhMzVlZjE5ZDM1NzE5NTZhNzIwNzU5MjgzMjg5NWU3YmNjZTNiNzMwM2I1OWEyZDVmODkwYjUwNjQ4MmVkYzM4MDM0NmRhMGUzZDU3OTI5MWYsZXlKamJHbGxiblJKWkNJNklpSXNJbTVoYldVaU9pSktjM0J5WldGa2MyaGxaWFFpTENKa1lYUmxJam94TnpnNU16ZzJPRFF5TENKa2IyMWhhVzRpT2xzaWFuTndjbVZoWkhOb1pXVjBMbU52YlNJc0ltTnZaR1Z6WVc1a1ltOTRMbWx2SWl3aWFuTm9aV3hzTG01bGRDSXNJbU56WWk1aGNIQWlMQ0p6ZEdGamEySnNhWFI2TG1sdklpd2lkMlZpWTI5dWRHRnBibVZ5TG1sdklpd2liRzlqWVd4b2IzTjBJbDBzSW5Cc1lXNGlPaUl6TkNJc0luTmpiM0JsSWpwYkluWTNJaXdpZGpnaUxDSjJPU0lzSW5ZeE1DSXNJbll4TVNJc0luWXhNaUlzSW5ZeE15SXNJbU5vWVhKMGN5SXNJbVp2Y20xeklpd2labTl5YlhWc1lTSXNJbkJoY25ObGNpSXNJbkpsYm1SbGNpSXNJbU52YlcxbGJuUnpJaXdpYVcxd2IzSjBaWElpTENKaVlYSWlMQ0oyWVd4cFpHRjBhVzl1Y3lJc0luTmxZWEpqYUNJc0luQnlhVzUwSWl3aWMyaGxaWFJ6SWl3aVkyeHBaVzUwSWl3aWMyVnlkbVZ5SWl3aWMyaGhjR1Z6SWl3aVptOXliV0YwSWl3aWNHbDJiM1FpWFN3aVpHVnRieUk2ZEhKMVpYMD0=');

export default {
    components: {
        Spreadsheet,
        Worksheet,
    },
    methods: {
        setUntrackedValue() {
            if (this.$refs.spreadsheet.current[0]) {
                jspreadsheet.history.ignore = true;
                this.$refs.spreadsheet.current[0].setValue('A1', Math.random() * 1000);
                jspreadsheet.history.ignore = false;
            }
        },
        undo() {
            jspreadsheet.history.undo();
        }
    }
}
</script>
import { Component, ViewChild, ElementRef } from '@angular/core';
import jspreadsheet from 'jspreadsheet';
import "jsuites/dist/jsuites.css";
import "jspreadsheet/dist/jspreadsheet.css";

// You can use the following license for quick testing on localhost, StackBlitz, or CodeSandbox.
// The license is valid for one day, after which the spreadsheet will become read-only.
// For a longer trial period, you can create a free account and generate a demo license with an extended expiration date.
jspreadsheet.setLicense('MGQ2MmIzN2VlMGM2ODRiMDY1OTc3MGRiMzY4OTJhMzk5ZjY4MmQyMzUxZmE2MzRhMzVlZjE5ZDM1NzE5NTZhNzIwNzU5MjgzMjg5NWU3YmNjZTNiNzMwM2I1OWEyZDVmODkwYjUwNjQ4MmVkYzM4MDM0NmRhMGUzZDU3OTI5MWYsZXlKamJHbGxiblJKWkNJNklpSXNJbTVoYldVaU9pSktjM0J5WldGa2MyaGxaWFFpTENKa1lYUmxJam94TnpnNU16ZzJPRFF5TENKa2IyMWhhVzRpT2xzaWFuTndjbVZoWkhOb1pXVjBMbU52YlNJc0ltTnZaR1Z6WVc1a1ltOTRMbWx2SWl3aWFuTm9aV3hzTG01bGRDSXNJbU56WWk1aGNIQWlMQ0p6ZEdGamEySnNhWFI2TG1sdklpd2lkMlZpWTI5dWRHRnBibVZ5TG1sdklpd2liRzlqWVd4b2IzTjBJbDBzSW5Cc1lXNGlPaUl6TkNJc0luTmpiM0JsSWpwYkluWTNJaXdpZGpnaUxDSjJPU0lzSW5ZeE1DSXNJbll4TVNJc0luWXhNaUlzSW5ZeE15SXNJbU5vWVhKMGN5SXNJbVp2Y20xeklpd2labTl5YlhWc1lTSXNJbkJoY25ObGNpSXNJbkpsYm1SbGNpSXNJbU52YlcxbGJuUnpJaXdpYVcxd2IzSjBaWElpTENKaVlYSWlMQ0oyWVd4cFpHRjBhVzl1Y3lJc0luTmxZWEpqYUNJc0luQnlhVzUwSWl3aWMyaGxaWFJ6SWl3aVkyeHBaVzUwSWl3aWMyVnlkbVZ5SWl3aWMyaGhjR1Z6SWl3aVptOXliV0YwSWl3aWNHbDJiM1FpWFN3aVpHVnRieUk2ZEhKMVpYMD0=');

@Component({
  standalone: true,
  selector: 'app-root',
  template: `<div #spreadsheet></div>
        <input type="button" value="Set A1 (untracked)" (click)="setUntrackedValue()" />
        <input type="button" value="Undo" (click)="undo()" />`,
})
export class AppComponent {
  @ViewChild('spreadsheet') spreadsheet: ElementRef;
  worksheets: jspreadsheet.worksheetInstance[];

  ngAfterViewInit() {
    this.worksheets = jspreadsheet(this.spreadsheet.nativeElement, {
      worksheets: [
        {
          minDimensions: [6, 6],
        },
      ],
    });
  }

  setUntrackedValue(): void {
    jspreadsheet.history.ignore = true;
    this.worksheets[0].setValue('A1', Math.random() * 1000);
    jspreadsheet.history.ignore = false;
  }

  undo(): void {
    jspreadsheet.history.undo();
  }
}

Cascading History Actions

This example shows how to group multiple changes into a single undo action using the cascade flag.

<html>
<script src="https://jspreadsheet.com/v13/jspreadsheet.js"></script>
<script src="https://jsuites.net/v6/jsuites.js"></script>
<link rel="stylesheet" href="https://jspreadsheet.com/v13/jspreadsheet.css" type="text/css" />
<link rel="stylesheet" href="https://jsuites.net/v6/jsuites.css" type="text/css" />

<div id='spreadsheet'></div>

<p><input type="button" value="Set Value & Style (cascaded)" id="btn1" class="jss_object" />
<input type="button" value="Undo" id="btn2" class="jss_object" /></p>

<script>
// You can use the following license for quick testing on localhost, StackBlitz, or CodeSandbox.
// The license is valid for one day, after which the spreadsheet will become read-only.
// For a longer trial period, you can create a free account and generate a demo license with an extended expiration date.
jspreadsheet.setLicense('MGQ2MmIzN2VlMGM2ODRiMDY1OTc3MGRiMzY4OTJhMzk5ZjY4MmQyMzUxZmE2MzRhMzVlZjE5ZDM1NzE5NTZhNzIwNzU5MjgzMjg5NWU3YmNjZTNiNzMwM2I1OWEyZDVmODkwYjUwNjQ4MmVkYzM4MDM0NmRhMGUzZDU3OTI5MWYsZXlKamJHbGxiblJKWkNJNklpSXNJbTVoYldVaU9pSktjM0J5WldGa2MyaGxaWFFpTENKa1lYUmxJam94TnpnNU16ZzJPRFF5TENKa2IyMWhhVzRpT2xzaWFuTndjbVZoWkhOb1pXVjBMbU52YlNJc0ltTnZaR1Z6WVc1a1ltOTRMbWx2SWl3aWFuTm9aV3hzTG01bGRDSXNJbU56WWk1aGNIQWlMQ0p6ZEdGamEySnNhWFI2TG1sdklpd2lkMlZpWTI5dWRHRnBibVZ5TG1sdklpd2liRzlqWVd4b2IzTjBJbDBzSW5Cc1lXNGlPaUl6TkNJc0luTmpiM0JsSWpwYkluWTNJaXdpZGpnaUxDSjJPU0lzSW5ZeE1DSXNJbll4TVNJc0luWXhNaUlzSW5ZeE15SXNJbU5vWVhKMGN5SXNJbVp2Y20xeklpd2labTl5YlhWc1lTSXNJbkJoY25ObGNpSXNJbkpsYm1SbGNpSXNJbU52YlcxbGJuUnpJaXdpYVcxd2IzSjBaWElpTENKaVlYSWlMQ0oyWVd4cFpHRjBhVzl1Y3lJc0luTmxZWEpqYUNJc0luQnlhVzUwSWl3aWMyaGxaWFJ6SWl3aVkyeHBaVzUwSWl3aWMyVnlkbVZ5SWl3aWMyaGhjR1Z6SWl3aVptOXliV0YwSWl3aWNHbDJiM1FpWFN3aVpHVnRieUk2ZEhKMVpYMD0=');

// Create the spreadsheet
let spreadsheet = jspreadsheet(document.getElementById('spreadsheet'), {
    worksheets: [{
        minDimensions: [6, 6],
    }],
});

document.getElementById("btn1").onclick = () => {
    // Generate random cell coordinates
    let col = Math.floor(Math.random() * 6);
    let row = Math.floor(Math.random() * 6);
    let cellName = jspreadsheet.helpers.getCellNameFromCoords(col, row);

    // Enable cascade to group multiple actions
    spreadsheet[0].setValue(cellName, 'test');
    jspreadsheet.history.cascade = true;
    spreadsheet[0].setStyle(cellName, 'background-color: red');
    jspreadsheet.history.cascade = false;
}

document.getElementById("btn2").onclick = () => {
    jspreadsheet.history.undo();
}
</script>
</html>
import React, {useRef} from "react";
import { Spreadsheet, Worksheet, jspreadsheet } from "@jspreadsheet/react";
import "jsuites/dist/jsuites.css";
import "jspreadsheet/dist/jspreadsheet.css";

// You can use the following license for quick testing on localhost, StackBlitz, or CodeSandbox.
// The license is valid for one day, after which the spreadsheet will become read-only.
// For a longer trial period, you can create a free account and generate a demo license with an extended expiration date.
jspreadsheet.setLicense('MGQ2MmIzN2VlMGM2ODRiMDY1OTc3MGRiMzY4OTJhMzk5ZjY4MmQyMzUxZmE2MzRhMzVlZjE5ZDM1NzE5NTZhNzIwNzU5MjgzMjg5NWU3YmNjZTNiNzMwM2I1OWEyZDVmODkwYjUwNjQ4MmVkYzM4MDM0NmRhMGUzZDU3OTI5MWYsZXlKamJHbGxiblJKWkNJNklpSXNJbTVoYldVaU9pSktjM0J5WldGa2MyaGxaWFFpTENKa1lYUmxJam94TnpnNU16ZzJPRFF5TENKa2IyMWhhVzRpT2xzaWFuTndjbVZoWkhOb1pXVjBMbU52YlNJc0ltTnZaR1Z6WVc1a1ltOTRMbWx2SWl3aWFuTm9aV3hzTG01bGRDSXNJbU56WWk1aGNIQWlMQ0p6ZEdGamEySnNhWFI2TG1sdklpd2lkMlZpWTI5dWRHRnBibVZ5TG1sdklpd2liRzlqWVd4b2IzTjBJbDBzSW5Cc1lXNGlPaUl6TkNJc0luTmpiM0JsSWpwYkluWTNJaXdpZGpnaUxDSjJPU0lzSW5ZeE1DSXNJbll4TVNJc0luWXhNaUlzSW5ZeE15SXNJbU5vWVhKMGN5SXNJbVp2Y20xeklpd2labTl5YlhWc1lTSXNJbkJoY25ObGNpSXNJbkpsYm1SbGNpSXNJbU52YlcxbGJuUnpJaXdpYVcxd2IzSjBaWElpTENKaVlYSWlMQ0oyWVd4cFpHRjBhVzl1Y3lJc0luTmxZWEpqYUNJc0luQnlhVzUwSWl3aWMyaGxaWFJ6SWl3aVkyeHBaVzUwSWl3aWMyVnlkbVZ5SWl3aWMyaGhjR1Z6SWl3aVptOXliV0YwSWl3aWNHbDJiM1FpWFN3aVpHVnRieUk2ZEhKMVpYMD0=');

export default function App() {
    const spreadsheet = useRef();

    const setCascadedValueAndStyle = () => {
        if (spreadsheet.current) {
            // Generate random cell coordinates
            let col = Math.floor(Math.random() * 6);
            let row = Math.floor(Math.random() * 6);
            let cellName = jspreadsheet.helpers.getCellNameFromCoords(col, row);

            // Enable cascade to group multiple actions
            spreadsheet.current[0].setValue(cellName, 'test');
            jspreadsheet.history.cascade = true;
            spreadsheet.current[0].setStyle(cellName, 'background-color: red');
            jspreadsheet.history.cascade = false;
        }
    };

    const handleUndo = () => {
        jspreadsheet.history.undo();
    };

    return (
        <>
            <Spreadsheet ref={spreadsheet}>
                <Worksheet minDimensions={[6, 6]} />
            </Spreadsheet>
            <input type="button" value="Set Value & Style (cascaded)" onClick={setCascadedValueAndStyle} className="jss_object"/>
            <input type="button" value="Undo" onClick={handleUndo} className="jss_object"/>
        </>
    );
}
<template>
    <Spreadsheet ref="spreadsheet">
        <Worksheet :minDimensions="[6,6]" />
    </Spreadsheet>
    <input type="button" value="Set Value & Style (cascaded)" @click="setCascadedValueAndStyle" class="jss_object" />
    <input type="button" value="Undo" @click="undo" class="jss_object" />
</template>

<script>
import { Spreadsheet, Worksheet, jspreadsheet } from "@jspreadsheet/vue";
import "jsuites/dist/jsuites.css";
import "jspreadsheet/dist/jspreadsheet.css";

// You can use the following license for quick testing on localhost, StackBlitz, or CodeSandbox.
// The license is valid for one day, after which the spreadsheet will become read-only.
// For a longer trial period, you can create a free account and generate a demo license with an extended expiration date.
jspreadsheet.setLicense('MGQ2MmIzN2VlMGM2ODRiMDY1OTc3MGRiMzY4OTJhMzk5ZjY4MmQyMzUxZmE2MzRhMzVlZjE5ZDM1NzE5NTZhNzIwNzU5MjgzMjg5NWU3YmNjZTNiNzMwM2I1OWEyZDVmODkwYjUwNjQ4MmVkYzM4MDM0NmRhMGUzZDU3OTI5MWYsZXlKamJHbGxiblJKWkNJNklpSXNJbTVoYldVaU9pSktjM0J5WldGa2MyaGxaWFFpTENKa1lYUmxJam94TnpnNU16ZzJPRFF5TENKa2IyMWhhVzRpT2xzaWFuTndjbVZoWkhOb1pXVjBMbU52YlNJc0ltTnZaR1Z6WVc1a1ltOTRMbWx2SWl3aWFuTm9aV3hzTG01bGRDSXNJbU56WWk1aGNIQWlMQ0p6ZEdGamEySnNhWFI2TG1sdklpd2lkMlZpWTI5dWRHRnBibVZ5TG1sdklpd2liRzlqWVd4b2IzTjBJbDBzSW5Cc1lXNGlPaUl6TkNJc0luTmpiM0JsSWpwYkluWTNJaXdpZGpnaUxDSjJPU0lzSW5ZeE1DSXNJbll4TVNJc0luWXhNaUlzSW5ZeE15SXNJbU5vWVhKMGN5SXNJbVp2Y20xeklpd2labTl5YlhWc1lTSXNJbkJoY25ObGNpSXNJbkpsYm1SbGNpSXNJbU52YlcxbGJuUnpJaXdpYVcxd2IzSjBaWElpTENKaVlYSWlMQ0oyWVd4cFpHRjBhVzl1Y3lJc0luTmxZWEpqYUNJc0luQnlhVzUwSWl3aWMyaGxaWFJ6SWl3aVkyeHBaVzUwSWl3aWMyVnlkbVZ5SWl3aWMyaGhjR1Z6SWl3aVptOXliV0YwSWl3aWNHbDJiM1FpWFN3aVpHVnRieUk2ZEhKMVpYMD0=');

export default {
    components: {
        Spreadsheet,
        Worksheet,
    },
    methods: {
        setCascadedValueAndStyle() {
            if (this.$refs.spreadsheet.current[0]) {
                // Generate random cell coordinates
                let col = Math.floor(Math.random() * 6);
                let row = Math.floor(Math.random() * 6);
                let cellName = jspreadsheet.helpers.getCellNameFromCoords(col, row);

                // Enable cascade to group multiple actions
                this.$refs.spreadsheet.current[0].setValue(cellName, 'test');
                jspreadsheet.history.cascade = true;
                this.$refs.spreadsheet.current[0].setStyle(cellName, 'background-color: red');
                jspreadsheet.history.cascade = false;
            }
        },
        undo() {
            jspreadsheet.history.undo();
        }
    }
}
</script>
import { Component, ViewChild, ElementRef } from '@angular/core';
import jspreadsheet from 'jspreadsheet';
import "jsuites/dist/jsuites.css";
import "jspreadsheet/dist/jspreadsheet.css";

// You can use the following license for quick testing on localhost, StackBlitz, or CodeSandbox.
// The license is valid for one day, after which the spreadsheet will become read-only.
// For a longer trial period, you can create a free account and generate a demo license with an extended expiration date.
jspreadsheet.setLicense('MGQ2MmIzN2VlMGM2ODRiMDY1OTc3MGRiMzY4OTJhMzk5ZjY4MmQyMzUxZmE2MzRhMzVlZjE5ZDM1NzE5NTZhNzIwNzU5MjgzMjg5NWU3YmNjZTNiNzMwM2I1OWEyZDVmODkwYjUwNjQ4MmVkYzM4MDM0NmRhMGUzZDU3OTI5MWYsZXlKamJHbGxiblJKWkNJNklpSXNJbTVoYldVaU9pSktjM0J5WldGa2MyaGxaWFFpTENKa1lYUmxJam94TnpnNU16ZzJPRFF5TENKa2IyMWhhVzRpT2xzaWFuTndjbVZoWkhOb1pXVjBMbU52YlNJc0ltTnZaR1Z6WVc1a1ltOTRMbWx2SWl3aWFuTm9aV3hzTG01bGRDSXNJbU56WWk1aGNIQWlMQ0p6ZEdGamEySnNhWFI2TG1sdklpd2lkMlZpWTI5dWRHRnBibVZ5TG1sdklpd2liRzlqWVd4b2IzTjBJbDBzSW5Cc1lXNGlPaUl6TkNJc0luTmpiM0JsSWpwYkluWTNJaXdpZGpnaUxDSjJPU0lzSW5ZeE1DSXNJbll4TVNJc0luWXhNaUlzSW5ZeE15SXNJbU5vWVhKMGN5SXNJbVp2Y20xeklpd2labTl5YlhWc1lTSXNJbkJoY25ObGNpSXNJbkpsYm1SbGNpSXNJbU52YlcxbGJuUnpJaXdpYVcxd2IzSjBaWElpTENKaVlYSWlMQ0oyWVd4cFpHRjBhVzl1Y3lJc0luTmxZWEpqYUNJc0luQnlhVzUwSWl3aWMyaGxaWFJ6SWl3aVkyeHBaVzUwSWl3aWMyVnlkbVZ5SWl3aWMyaGhjR1Z6SWl3aVptOXliV0YwSWl3aWNHbDJiM1FpWFN3aVpHVnRieUk2ZEhKMVpYMD0=');

@Component({
  standalone: true,
  selector: 'app-root',
  template: `<div #spreadsheet></div>
        <input type="button" value="Set Value & Style (cascaded)" (click)="setCascadedValueAndStyle()" class="jss_object" />
        <input type="button" value="Undo" (click)="undo()" class="jss_object" />`,
})
export class AppComponent {
  @ViewChild('spreadsheet') spreadsheet: ElementRef;
  worksheets: jspreadsheet.worksheetInstance[];

  ngAfterViewInit() {
    this.worksheets = jspreadsheet(this.spreadsheet.nativeElement, {
      worksheets: [
        {
          minDimensions: [6, 6],
        },
      ],
    });
  }

  setCascadedValueAndStyle(): void {
    // Generate random cell coordinates
    let col = Math.floor(Math.random() * 6);
    let row = Math.floor(Math.random() * 6);
    let cellName = jspreadsheet.helpers.getCellNameFromCoords(col, row);

    // Enable cascade to group multiple actions
    this.worksheets[0].setValue(cellName, 'test');
    jspreadsheet.history.cascade = true;
    this.worksheets[0].setStyle(cellName, 'background-color: red');
    jspreadsheet.history.cascade = false;
  }

  undo(): void {
    jspreadsheet.history.undo();
  }
}