↩️ Native Undo & Redo for the Web
Tags: undo, redo, execcommand
Published: 2018-04-23 13:20:06

---
title: ↩️ Native Undo & Redo for the Web
published: true
description: Learn how to support native undo and redo events for any interaction, not just inputs and text fields.
tags: undo, redo, execCommand
cover_image: https://thepracticaldev.s3.amazonaws.com/i/mwsqsnmvbu67x2fo9ytn.jpg
---

How can you support Undo and Redo natively for elements that don't support it by default? Think drag and drop reordering, games, or rich text editors where the built-in support just doesn't cut it. 🙅✂️📄

This is possible today using a bit of logic coupled with the `execCommand` method. First, a demo! Click/focus the maze, and navigate with your arrow keys (or use the buttons). Most importantly, be sure to try Undo and Redo to go back and forward.

{% codepen https://codepen.io/samthor/pen/WJvLxd %}

There are a few ways to Undo—you probably tried `Ctrl/Cmd-Z`, but you can also use your browser's Undo menu item or right click on the page. On Mobile Safari 📱🤠, shake 👋 your phone to Undo ↩️.
(I'm not sure how to Undo on other mobile browsers.)

> <small>If you're just here for the code rather than the post, I've built an `Undoer` class, which provides helpers for native undo/redo. [Check it out here ⤵️💻](https://github.com/samthor/undoer)</small>

<hr />

# Why

If you're building an interactive app-like experience for the web, you should aim to provide as many built-in UX flows as possible, so that your users can be productive. These might include supporting the `ESC` key, the browser's inbuilt navigation buttons, drag and drop, or even native sharing via the [Web Share API](https://developers.google.com/web/updates/2016/09/navigator-share)—all idioms that even less savvy technical users are used to.

<hr />

# How

Under the hood, the maze above is actually modifying a text field—as a user would—and watching to see if it's value changes back to a previously recorded state. What does that mean?

![Undo/Redo stack](https://thepracticaldev.s3.amazonaws.com/i/b6chmm73xqvtd5mdj8id.png)

* As we make changes (the blue boxes above), we secretly do update a 'hidden' text field with a new number, which is an index into an array recording the position of the player.

* When the player performs an Undo (the red boxes), we pop the last event off the stack, and call the maze back with the previous state. In this case, state 1: `{x:2, y:1}`.

* The later states remain (in case the user calls Redo) until the user performs another action, when we erase them.

<hr />

# Build It

Let's plan out the flows. When the user uses their keyboard, we handle the event—both updating the maze and pushing a new state. And when the user performs undo or redo, we detect the change and restore the maze to whatever previous state we expect.

![Interaction flow](https://thepracticaldev.s3.amazonaws.com/i/el9uhd5793sdibujyy4s.png)

Ok! Let's build it.

## 1. The 'hidden' text field

We create a text field as normal, and append it to the page. We don't use `display: none` or `visibility: hidden` to hide the field—hiding the element "properly" makes it ineligible for a browser's normal undo/redo stack. So in order to make it feel unusable, we:

* set its `tabIndex` to -1, which removes it from normal tab order
* add a `focus` handler which immediately blurs the element, so it never retains focus
* can set its `opacity`/`position` such that the element isn't *visible*—although while you're testing, it can be good to keep it around

Here's how we add the `undoer` element:

```js
const undoer = document.createElement('input');
undoer.value = '0';
undoer.tabIndex = -1;  // don't allow automatic tab
undoer.addEventListener('focus', () => {
  window.setTimeout(() => undoer.blur(), 0);  // prevent focus, delay for Safari
});

// You could hide it (but not with `display: none`—it must be on the page):
//undoer.style.opacity = 0;
//undoer.style.position = 'absolute';

document.body.appendChild(undoer);
```

> Q: This input might confuse accessible user-agents. Please leave me a comment if you have ideas on how to solve this for screen readers etc!

## 2. Pretending to enter input

Let's create a method `pushNewUndoState` to push a new state onto the stack, and record these states. This is the key part of this post—we use `execCommand` to fake a user input. The code below pushes a new state onto the stack, and "types" the next number:

```js
let duringInput = false;
const undoStack = [{x: 1, y: 1}];  // we have to start with our initial state
function pushNewUndoState(data) {
  // remove states past now, add our new state
  const nextStateId = +undoer.value + 1;
  undoStack.splice(nextStateId, undoStack.length - nextStateId, data);

  duringInput = true;

  // focus and "type" the next number
  undoer.focus();
  document.execCommand('selectAll');
  document.execCommand('insertText', false, nextStateId);

  duringInput = false;
}
```

The `document.execCommand` is very powerful and allows us to emulate user input on the page. In this case, we just want to select the current number and type a new number. The browser is smart enough to coalesce these two unique actions into undo "undoable" event.

Note that:

* We focus on the `undoer`, but this is prevented by the event handler we set up initially

* You could add code to refocus on the previous `document.activeElement`, if you're using focus in your application: otherwise the focus will be lost

* The `.splice` method _removes_ any states past our current one. This happens if the user *undoes* things, then performs a new action—it removes the possible *redo* actions.

## 3. Listening to input events

Finally, let's listen to changes in the input so we can trigger a callback letting the maze know the user wants to revert their position:

```js
undoer.addEventListener('input', (ev) => {
  // nb. don't use 'change', it doesn't fire in all browsers
  if (!duringInput) {
    const data = undoStack[+undoer.value];
    updateMazeWithData(data);
  }
});
```

We don't want to trigger a callback `duringInput`, aka, when the `pushNewUndoState` method above is doing its work. We only want to know when the input has changed if the user performs an undo or redo.

## 4. Special-case for Firefox

Firefox has a [long-standing issue](https://bugzilla.mozilla.org/show_bug.cgi?id=1220696) that means that `execCommand` is only supported in a `contentEditable`. Supporting this is actually fairly easy—you can just swap out the 'hidden' `input` for an editable `div`, and use `.textContent` instead of `.value`—phew. That's explanation enough, but I've left the code changes out of this post.

[The code I've included](https://github.com/samthor/undoer) with this blog post just uses `<div contentEditable>`, but you could choose either approach. 🤔

<hr />

🎉 That's all there is to it, but read on for some additional thoughts ⬇️

# Extras

Keen observers might note that this is a very similar model to the History API, which uses `history.pushState` and `window.onpopstate`.

The `onpopstate` method is actually badly named—it is called whether the state is popped (the Back button) or *pushed* (the Forward action). The analogy in using native undo/redo is where we listen to the `input` event of our 'hidden' text input.

## Intercepting Undo & Redo for text fields

This approach to undo and redo can be extended to work on text fields or elements which generate *their own* undo events. Why would you want to do this? As I mentioned in the very first paragraph, maybe you're building a rich text editor, and undo and redo isn't doing exactly what you want.

You can do this by using `execCommand` to immediately undo a user action, setting the updated value programmatically (which doesn't generate an undoable event) and then replace it with your own event, via our method `pushNewUndoState`.

For instance, we can piggyback the `input` event:

```js
theInput.addEventListener('input', (ev) => {
  const currentValue = theInput.value;
  document.execCommand('undo');   // undo this change
  theInput.value = currentValue;  // ... but immediately reset the value
  // TODO: You might want to persist the selection position.

  pushNewUndoState(currentValue);  // ... our special state to keep
});
```

⚠️ This works well, but be careful with this approach! Unless you're sure that the user just changed the element you care about, you might be undoing some other user action on your page.

## The undo keyboard shortcuts

Many examples of custom undo & redo on the web use `keydown` handlers to detect whether the user is entering `Ctrl/Cmd-Z`, and performing a special action. This usually works but has two challenges:

* It doesn't capture undo and redo through a context menu, or through the browser's menu bar

* It's not accessible: if a user has rebound the keys to something else, or uses an alternative input device to undo or redo

These two reasons are fundamentally why it's worth making the browser work for us—through our 'hidden' input.

<hr />

# Thanks

Thanks for reading! As I mentioned above, I've turned this knowledge into a class you can use in your own apps, but hopefully the idioms above are simple enough to apply directly within your own code too. [Check the 'undoer' package out here ⤵️💻](https://github.com/samthor/undoer).

Ask me questions here or find me on Twitter.

## Acknowlegements

* Some icons from [Icons8.com](https://icons8.com/icon/set/undo/dusk), used under CC BY-ND.
* Emoji by [Emojityper](https://emojityper.com).