Undo and redo let you move back and forth through the changes you've made, such as edits to the text in an editor. Undo steps back one change, to the value you had before it. Redo steps forward one change, to the value you undid. Each one moves exactly one step, and when there's nothing to undo or redo, it does nothing, which is why editors grey those buttons out.
Picture a row of snapshots of your document, one taken after every change, and a finger on the one you're looking at. Undo moves the finger one snapshot back, and redo moves it one forward. Undoing doesn't throw anything away: the snapshots after the finger are still there, waiting to be redone.
The rule that surprises people is what a new change does after you've undone something. The snapshots ahead of your finger followed from a version you've now changed in a different way, so they're thrown away: you can't redo them any more, and the new value becomes the latest snapshot. Here's an empty text box, with every key press counted as one change:
| You | Value now | Can undo? | Can redo? | What happened |
|---|---|---|---|---|
| Start | "" | No | No | Nothing has changed yet. |
Type H | "H" | Yes | No | One change to step back from. |
Type i | "Hi" | Yes | No | Two changes now. |
| Undo | "H" | Yes | Yes | One step back. "Hi" is kept, so you can redo it. |
| Undo | "" | No | Yes | Back at the start, with nothing older to go back to. |
| Redo | "H" | Yes | Yes | One step forward again. |
Type o | "Ho" | Yes | No | A new change after undoing: "Hi" can't be redone any more. |
| Undo | "H" | Yes | Yes | Redo would now bring back "Ho", not "Hi". |
Real editors add two things. Many group a burst of typing into one step, so one undo takes back a whole word or more; above, every change is its own step. And many keep only a limited number of steps: past the limit, the oldest one is forgotten, and you can't undo back that far.
Undo is Ctrl+Z (Cmd+Z on a Mac) almost everywhere, and redo is usually Ctrl+Y or Ctrl+Shift+Z (Cmd+Shift+Z on a Mac). Browsers give text inputs undo and redo of their own, but anything whose state your code keeps, such as a drawing canvas, a form builder, or a to-do list, gets undo only if you build it.
Write a class UndoHistory that holds one current value and remembers how it got there. new UndoHistory(initial, options) starts with initial as the current value and nothing to undo or redo. Values can be anything, including undefined, null, and 0.
value is a getter for the current value.
push(value) makes value the current one. The old current value becomes the latest step you can undo, and everything you could redo is dropped, as in an editor when you type after undoing.
undo() steps back one value and redo() steps forward one. Both return the current value after the step. With nothing to undo or redo, they change nothing and return the current value.
canUndo and canRedo are getters that say whether undo() and redo() would do anything.
options.limit caps how many steps you can undo. There's no limit by default. When a push goes over the limit, the oldest step is forgotten.
const text = new UndoHistory("");
text.push("H");
text.push("Hi");
text.undo(); // "H"
text.undo(); // ""
text.redo(); // "H"
text.push("Ho"); // the redo step "Hi" is gone
text.canRedo; // false
const steps = new UndoHistory(0, { limit: 2 });
steps.push(1);
steps.push(2);
steps.push(3); // a third step goes over the limit: the step back to 0 is forgotten
steps.undo(); // 2
steps.undo(); // 1
steps.canUndo; // false
const history = new UndoHistory("a");
expect(history.value).toBe("a");
expect(history.canUndo).toBe(false);
expect(history.canRedo).toBe(false);
const history = new UndoHistory("a");
history.push("b");
expect(history.value).toBe("b");
expect(history.canUndo).toBe(true);
const history = new UndoHistory("a");
history.push("b");
history.push("c");
expect(history.undo()).toBe("b");
expect(history.undo()).toBe("a");
expect(history.value).toBe("a");
expect(history.canUndo).toBe(false);
expect(history.canRedo).toBe(true);
const history = new UndoHistory("a");
history.push("b");
history.push("c");
history.undo();
history.undo();
expect(history.redo()).toBe("b");
expect(history.redo()).toBe("c");
expect(history.canRedo).toBe(false);
const history = new UndoHistory("a");
history.push("b");
history.push("c");
history.undo();
history.push("d");
expect(history.canRedo).toBe(false);
expect(history.redo()).toBe("d");
expect(history.undo()).toBe("b");
const history = new UndoHistory(1);
expect(history.undo()).toBe(1);
expect(history.redo()).toBe(1);
expect(history.value).toBe(1);
expect(history.canUndo).toBe(false);
expect(history.canRedo).toBe(false);
const history = new UndoHistory(undefined);
history.push(0);
history.push(null);
expect(history.undo()).toBe(0);
expect(history.undo()).toBeUndefined();
expect(history.canUndo).toBe(false);
expect(history.redo()).toBe(0);
expect(history.redo()).toBeNull();
const history = new UndoHistory(0, { limit: 2 });
history.push(1);
history.push(2);
history.push(3);
expect(history.undo()).toBe(2);
expect(history.undo()).toBe(1);
expect(history.canUndo).toBe(false);
expect(history.undo()).toBe(1);
Run tests runs every test on your code, here in your browser. The problem is solved when they all pass.
Ctrl + Enter runs the tests (⌘ + Enter on a Mac).
I'm here to help with Undo/redo history. I give hints, never the solution, so you get to solve it yourself. Ask for a hint, select code to ask about it, or press Ask Tuto why on a failing test.
Your first 3 answers need no account. for free to keep asking.