Search code examples
javascripthandsontable

Handsonetable - undo/redo over multiple instances of component


I am thinking about using Handsontable (https://github.com/handsontable/handsontable) on my project.

I need to have multiple table components on the page,

and have two own buttons for undo/redo which handles undo/redo action over all components in same order as user did changes.

I mean it like user made change in table A then in table B and then again in table A. Then if he hits undo, then A is changed, after next hit B is changed and after next hit A is changed back.

Is it somehow possible to do that?


Solution

  • So that we can close this question and for people who would like to get advice on how to do this, here is a short idea of how to implement this feature:

    Assumptions:

    • We have instances of Handsontable which have undo and redo methods, defined below as per the Handsontable documentation.
    • We have two buttons with ids #undo and #redo to trigger the behavior.

    Methods:

    undo (): Undo last edit

    redo (): Redo edit (used to reverse an undo)


    Logic

    The technique is simple. We will be using two global arrays, abstract Queues, defined as undoQ and redoQ which will keep track of the reference of the last HOT instance to be modified. These Queues are modified every time the afterChange event is fired on any of the HOT instances, when the redo button is pressed, and when the undo button is pressed. To avoid over-complicating this approach, we will not be optimizing for space, but I will make a note of it so that if you need it, you can attempt to do it yourself.

    Disclaimer: you did not mention what would happen when one or the other table is in focus. The way HOT works is that ctrl-z, if enabled, will undo whichever table is in focus. You can disable this if you'd like.

    Detailed Implementation

    Step 1: Add event logic

    Add the afterChange event hook as follows, or append the logic if you already have a custom event; this goes in the declaration of the options of the HOT instance.

    afterChange: function() {
        return function(changes, source) {
            var hotInstance = this;
            // check that the `undo` event was initialized before pushing
            if (hotInstance.undo) {
                undoQ.push(this);
                redoQ = []; // IMPORTANT: this clears the redo array as do most undo operations, but you can add different logic if you'd like
            }
            // other logic if you have some
        }
    }
    

    This will push the REFERENCE to each table as a change occurs. Note that this array could get big but not terribly so. The reason is that the afterChange event is triggered only once per change. However, here is where the optimization can happen. What you could do instead, if you notice that you are triggering this event an absurd amount of times, is check if the last element in the Queues is the same as this, and if so, increase a counter rather than just append the reference. Then on undo or redo, you decrease the counters until they reach zero, and then pop.

    Step 2: Add undo and redo logic

    This step is a little more intuitive. What you want to do is add the following logic to the callback of the undo and redo button clicks.

    $("#undo").on("click", function() {
        if (undoQ.length > 0) {
            var hotInstance = undoQ.shift();
            hotInstance.undo();
            redoQ.push(hotInstance);
        }
    })
    
    $("#redo").on("click", function() {
        if (redoQ.length > 0) {
            var hotInstance = redoQ.shift();
            hotInstance.redo();
            undoQ.push(hotInstance);
        }
    })
    

    What we are doing here is manually triggering the respective events of whichever table was last changed. Remember that after a change, redoQ is emptied but as long as there aren't any, you should be able to undo and redo with no problem.

    That should be it. Good luck!