DEV Community

Cover image for How to implement undo in your apps
Taha Shashtari
Taha Shashtari

Posted on • Updated on • Originally published at tahazsh.com

How to implement undo in your apps

The command pattern is a very popular design pattern that was introduced in the GoF design patterns book. The main use case for this pattern is to decouple the code for handling some request from the code invoking it.

A common example of this is extracting the code in your page controller to a command object and execute it there. Doing this allows you to replace that command object with another one at runtime to modify the behavior of that request dynamically.

export function handleGetRequest({ request }) {
  const command = new ExampleCommand(request.data)
  command.execute()
}
Enter fullscreen mode Exit fullscreen mode

Another use case for the command pattern is to replace your long, complex function with a command object. In the command class, you can simplify the code a lot by splitting your code into smaller methods with clear intent and focus.

This might be more helpful in languages that don't have functions as first-class citizens, like Java. In JavaScript you don't have to use commands for that purpose (although you can); you can instead use nested functions.

Another good use case for commands—which is what I'm going to talk about in this article—is supporting undo functionality.

The source code for the example

I think it will be easier to follow along when you see the whole code of this article's example. So I encourage you to get it from GitHub and take a look at it before continuing.

Defining the example

Before showing you how to use commands for supporting undos, I need to show you the example I will use it on.

The example is a simple editor that uses a <textarea> element for editing the text. The editor has a toolbar with three buttons: undo, bold, and italic.

To make some text bold or italic, you need to select the text in the editor, and then click bold or italic. Clicking bold will wrap the selected text with <strong></strong>; whereas clicking italic will wrap it with <i></i>.

In this example, I'm encapsulating the textarea element with a class called Editor. In this class I have all the needed code for wrapping the selected text with some wrapping text.

You don't have to understand how it works; you just need to know that calling boldSelection or italicizeSelection makes the selected text bold or italic.

// Editor.js

export class Editor {
  #textareaElement

  constructor(textareaElement) {
    this.#textareaElement = textareaElement
  }

  get content() {
    return this.#textareaElement.value
  }

  set content(value) {
    this.#textareaElement.value = value
  }

  get #selectedText() {
    return this.content.slice(
      this.selectionRange.start,
      this.selectionRange.end
    )
  }

  get selectionRange() {
    return {
      start: this.#textareaElement.selectionStart,
      end: this.#textareaElement.selectionEnd
    }
  }

  select(selectionRange) {
    this.#textareaElement.focus()
    this.#textareaElement.setSelectionRange(
      selectionRange.start,
      selectionRange.end
    )
  }

  get #hasSelection() {
    return this.selectionRange.start !== this.selectionRange.end
  }

  #wrapSelectionWith(wrapperStart, wrapperEnd) {
    if (!this.#hasSelection) return

    const previousSelection = {
      start: this.selectionRange.start + wrapperStart.length,
      end: this.selectionRange.end + wrapperStart.length
    }

    const textBeforeSelection = this.content.slice(0, this.selectionRange.start)
    const textAfterSelection = this.content.slice(this.selectionRange.end)
    const wrappedText = wrapperStart + this.#selectedText + wrapperEnd
    this.content = textBeforeSelection + wrappedText + textAfterSelection

    requestAnimationFrame(() => {
      this.select(previousSelection)
    })
  }

  boldSelection() {
    this.#wrapSelectionWith('<strong>', '</strong>')
  }

  italicizeSelection() {
    this.#wrapSelectionWith('<i>', '</i>')
  }
}
Enter fullscreen mode Exit fullscreen mode

I have this html for this example:

<body>
  <div id="app">
    <div class="editor">
      <div class="toolbar">
        <button id="undo">undo</button>
        <button id="bold">bold</button>
        <button id="italic">italic</button>
      </div>
      <textarea id="textarea"></textarea>
    </div>
  </div>

  <script type="module" src="./index.js"></script>
  <script type="module">
    import { init } from './index.js'
    init()
  </script>
</body>
Enter fullscreen mode Exit fullscreen mode

Notice how I call init() to initialize the app state. This function is defined in index.js like this:

// index.js

import { Editor } from './Editor.js'

export function init() {
  const undo = document.getElementById('undo')
  const bold = document.getElementById('bold')
  const italic = document.getElementById('italic')
  const textarea = document.getElementById('textarea')

  const editor = new Editor(textarea)

  bold.addEventListener('mousedown', () => {
    editor.boldSelection()
  })

  italic.addEventListener('mousedown', () => {
    editor.italicizeSelection()
  })
}
Enter fullscreen mode Exit fullscreen mode

Support undo with the command pattern

First, I need to extract each editor action into a command. The command is a simple class with an execute method. Let's start with bold.

// EditorCommand.js

class BoldCommand {
  #editor
  constructor(editor) {
    this.#editor = editor
  }

  execute() {
    this.#editor.boldSelection()
  }
}
Enter fullscreen mode Exit fullscreen mode

Then I need to update index.js to use it instead of using the editor object directly.

// index.js

import { Editor } from './Editor.js'
import { BoldCommand } from './EditorCommand.js'

export function init() {
  //...
  const editor = new Editor(textarea)

  bold.addEventListener('mousedown', () => {
    const command = new BoldCommand(editor)
    command.execute()
  })
}
Enter fullscreen mode Exit fullscreen mode

The BoldCommand has a direct reference to the editor. This means I can use the editor in the context of that command however I want. So if the execute method is just for making the text bold, this means I can add any other method to modify the editor in that context. Adding undo is a perfect example of that.

// EditorCommand.js

class BoldCommand {
  #editor
  #previousContent

  constructor(editor) {
    this.#editor = editor
    this.#previousContent = this.#editor.content
  }

  execute() {
    this.#editor.boldSelection()
  }

  undo() {
    this.#editor.content = this.#previousContent
  }
}
Enter fullscreen mode Exit fullscreen mode

When BoldCommand is created, I save the content of that editor in the #previousContent field. Calling execute would modify the editor content using this.#editor.boldSelection(), while previousContent still contains the previous content before executing the command. Calling undo after that will set the content of the editor to the previous content; thus removing the bold text.

In index.js you can add an event listener to the undo button to call undo() on the bold command.

// index.js

import { Editor } from './Editor.js'
import { BoldCommand } from './EditorCommand.js'

export function init() {
  //...
  const editor = new Editor(textarea)
  const boldCommand = new BoldCommand(editor)

  bold.addEventListener('mousedown', () => {
    boldCommand.execute()
  })

  undo.addEventListener('click', () => {
    boldCommand.undo()
  })
}
Enter fullscreen mode Exit fullscreen mode

That would work for that specific command. But in real-world projects, the undo button should work on multiple commands with different types. That's what the command manager is for.

The command manager is an object that has an array in which it stores the executed commands. Storing the executed commands in it allows you to keep track of the commands that you want to call undo on. That array should work as a stack—LIFO (last in, first out). To do this, you need to call push to add a new command and call pop to take out a command to call undo on.

// CommandManager.js

export class CommandManager {
  #commands = []

  execute(command) {
    command.execute()
    this.#commands.push(command)
  }

  undo() {
    if (this.#commands.length <= 0) return

    const command = this.#commands.pop()
    command.undo()
  }
}
Enter fullscreen mode Exit fullscreen mode

After introducing the CommandManager to your code, you need to execute the commands through it—instead of individually—to store the command in the commands stack.

As shown in the code above, calling undo on the CommandManager will take the last command and call undo on it.

Now let's update index.js to use the command manager.

// index.js

import { Editor } from './Editor.js'
import { CommandManager } from './CommandManager.js'
import { BoldCommand } from './EditorCommand.js'

export function init() {
  //...

  const editor = new Editor(textarea)
  const commandManager = new CommandManager()

  bold.addEventListener('mousedown', () => {
    commandManager.execute(new BoldCommand(editor))
  })

  undo.addEventListener('click', () => {
    commandManager.undo()
  })
}
Enter fullscreen mode Exit fullscreen mode

The undo button should work as expected for the bold command. Next, let's do the same for italic button.

Add ItalicizeCommand

The ItalicizeCommand will be the same as BoldCommand except the execute method. This means I can DRY up the code by creating a super class to inherit from. Let's call it EditorCommand.

// EditorCommand.js

class EditorCommand {
  _editor
  #previousContent
  constructor(editor) {
    this._editor = editor
    this.#previousContent = this._editor.content
  }

  execute() {
    throw new Error('execute is an abstract method')
  }

  undo() {
    this._editor.content = this.#previousContent
  }
}

export class BoldCommand extends EditorCommand {
  execute() {
    this._editor.boldSelection()
  }
}

export class ItalicizeCommand extends EditorCommand {
  execute() {
    this._editor.italicizeSelection()
  }
}
Enter fullscreen mode Exit fullscreen mode

Finally, you need to update index.js to use it.

// index.js

import { Editor } from './Editor.js'
import { CommandManager } from './CommandManager.js'
import { BoldCommand, ItalicizeCommand } from './EditorCommand.js'

export function init() {
  const undo = document.getElementById('undo')
  const bold = document.getElementById('bold')
  const italic = document.getElementById('italic')
  const textarea = document.getElementById('textarea')

  const editor = new Editor(textarea)
  const commandManager = new CommandManager()

  bold.addEventListener('mousedown', () => {
    commandManager.execute(new BoldCommand(editor))
  })

  italic.addEventListener('mousedown', () => {
    commandManager.execute(new ItalicizeCommand(editor))
  })

  undo.addEventListener('click', () => {
    commandManager.undo()
  })
}
Enter fullscreen mode Exit fullscreen mode

Now the editor supports undo for both bold and italic. If you want to add a new feature to the editor that is undoable, you need to create a new command that inherits from EditorCommand, put the code for that feature in the execute method, and then call it through the CommandManager like above.


Thanks for Reading ❤️! Follow me on Twitter for updates about my latest blog posts, video tutorials, and some cool web dev tips. Let's be friends!

Top comments (4)

Collapse
 
mandrewdarts profile image
M. Andrew Darts

I really like this example of the command pattern. It's something I have always heard around but not seen many real world examples. Thanks!

Collapse
 
tahazsh profile image
Taha Shashtari

Thanks! I'm really glad you liked it :)

Collapse
 
corners2wall profile image
Corners 2 Wall

I think, that would be better attach code sample because it's complex hold in mind all this files

Collapse
 
tahazsh profile image
Taha Shashtari

You are right, thanks for your note. I uploaded the whole source code on GitHub.