DEV Community

Cover image for Add undo & redo functionality in your APP
Shuvo
Shuvo

Posted on

Add undo & redo functionality in your APP

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

0shuvo0 image

Was it helpful? Support me on Patreon

Patreon Logo

Discussion (2)

Collapse
jancizmar profile image
Jan Cizmar

Maybe you should also clean the redoQueue in execute method.

Collapse
0shuvo0 profile image
Shuvo Author

Might be a good idea