How to implement undo in your apps
Tags: javascript, programming, webdev, tutorial
Published: 2023-03-26 18:57:40

The command pattern is a very popular design pattern that was introduced in [the GoF design patterns book](https://en.wikipedia.org/wiki/Design_Patterns). 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.

```js
export function handleGetRequest({ request }) {
  const command = new ExampleCommand(request.data)
  command.execute()
}
```

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](https://tahazsh.com/blog/make-long-functions-more-readable#the-nested-functions-approach).

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](https://github.com/TahaSh/undo-with-command-pattern) 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.

```js
// 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>')
  }
}
```

I have this html for this example:

```html
<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>
```

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

```js
// 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()
  })
}
```

## 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.

```js
// EditorCommand.js

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

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

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

```js
// 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()
  })
}
```

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.

```js
// 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
  }
}
```

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.

```js
// 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()
  })
}
```

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.

```js
// 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()
  }
}
```

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*.

```js
// 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()
  })
}
```

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`.

```js
// 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()
  }
}
```

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

```js
// 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()
  })
}
```

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](https://twitter.com/tahazsh) for updates about my latest blog posts, video tutorials, and some cool web dev tips. Let's be friends!