Add undo & redo functionality in your APP

Shuvo - Nov 14 '21 - - Dev Community

halo! Opps I spelled it wrong. Let me Ctrl+z this. Ohh wait your app doesn't support undo and redo, what a shame :( Okay in this article lets fix that.
But instead of working with a big and complex project lets use this simple class that does some calculations.

class Calculate {
    constructor(initialValue){
        this.value = initialValue
    }
    add(val){
        this.value += val
    }
    sub(val){
        this.value -= val
    }
    mul(val){
        this.value *= val
    }
    div(val){
        this.value /= val
    }
}


const num = new Calculate(0)
num.add(10) // Value: 0 + 10 = 10
num.add(20) // Value: 10 + 20 = 30
num.mul(2) // Value: 30 * 2 = 60
num.sub(100) // Value: 60 - 100 = -40
console.log(num.value) // Output: -40
Enter fullscreen mode Exit fullscreen mode

Now we want to add undo and redo functionality to our class.
So basically we should be able to so this

const num = new Calculate(0)
num.add(10)
num.add(20)
num.mul(2)
num.sub(100)

num.undo()
num.undo()
num.redo()

console.log(num.value) // Expected output: 60
Enter fullscreen mode Exit fullscreen mode

Okay so first we will create another class called Executor that will have a execute function that take two functions:

  1. Function that does the indented operation
  2. Function that undo the operation
class Executor {
    constructor(){
        // Stores the undo functions
        this.undoQueue = []
        // Stores the redo functions
        this.redoQueue = []
    }
    execute(fn, undoFn){
        fn()
        this.undoQueue.push(() => {
            undoFn()
            // The redo will be added to queue only after the undo is executed
            this.redoQueue.push(fn) // calling fn after undoFn will be redoing the same operation
        })
    }
    undo(){
        if(this.undoQueue.length > 0){
            this.undoQueue.pop()()
        }
    }
    redo(){
        if(this.redoQueue.length > 0){
            this.redoQueue.pop()()
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

So now lets use the Executor in our Calculate class.

class Calculate {
    constructor(initialValue){
        this.value = initialValue
        this.executor = new Executor()
    }
    add(val){
        this.executor.execute(
            () => this.value += val, //Function to execute
            () => this.value -= val //Function to undo
        )
    }
    sub(val){
        this.executor.execute(
            () => this.value -= val, //Function to execute
            () => this.value += val //Function to undo
        )
    }
    mul(val){
        this.executor.execute(
            () => this.value *= val, //Function to execute
            () => this.value /= val //Function to undo
        )
    }
    div(val){
        this.executor.execute(
            () => this.value /= val, //Function to execute
            () => this.value *= val //Function to undo
        )
    }
    redo(){
        this.executor.redo()
    }
    undo(){
        this.executor.undo()
    }
}
Enter fullscreen mode Exit fullscreen mode

So now we can simply use num.undo() to undo the operation and num.redo() to redo it.
So here is the full code

class Calculate {
    constructor(initialValue){
        this.value = initialValue
        this.executor = new Executor()
    }
    add(val){
        this.executor.execute(
            () => this.value += val,
            () => this.value -= val
        )
    }
    sub(val){
        this.executor.execute(
            () => this.value -= val,
            () => this.value += val
        )
    }
    mul(val){
        this.executor.execute(
            () => this.value *= val,
            () => this.value /= val
        )
    }
    div(val){
        this.executor.execute(
            () => this.value /= val,
            () => this.value *= val
        )
    }
    redo(){
        this.executor.redo()
    }
    undo(){
        this.executor.undo()
    }
}

class Executor {
    constructor(){
        this.undoQueue = []
        this.redoQueue = []
    }
    execute(fn, undoFn){
        fn()
        this.undoQueue.push(() => {
            undoFn()
            this.redoQueue.push(fn)
        })
    }
    undo(){
        if(this.undoQueue.length > 0){
            this.undoQueue.pop()()
        }
    }
    redo(){
        if(this.redoQueue.length > 0){
            this.redoQueue.pop()()
        }
    }
}

const num = new Calculate(0)
num.add(10)
num.add(20)
num.mul(2)
num.sub(100)

num.undo()
num.undo()
num.redo()

console.log(num.value) // Output: 60
Enter fullscreen mode Exit fullscreen mode

Make sure you checkout my other articles and YouTube channel

Was it helpful? Support me on Patreon

Patreon Logo

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .