24

I want to add a undo/redo function in my script. I have looked around and see some suggestions, most of them recommended to use command pattern.

The function must work over one page - after a reload of the page the function must able to redo/undo the last things.

I don't know how command pattern works, I think about to create a object, to store the a name of the function, the old and the new value - but I'm not sure if this is a efficient way to do this or not.

Maybe somebody could give me a small example how the code for a undo/redo function should look.

Brian Burns
  • 20,575
  • 8
  • 83
  • 77
RainerS
  • 423
  • 1
  • 4
  • 11

1 Answers1

59

There's 2 common ways of implementing undo/redo:

It's easy to implement, but memory-inefficient since you need to store similar copies of the whole state.

  • The Command Pattern, where you capture commands/actions that affect the state; the current action and it's inverse action.

This is harder to implement since for for each undoable action in your application you must explicitly code it's inverse action, but it's far more memory-efficient since you only store the actions that affect the state.

Now let's elaborate on the 2 patterns:

The Memento Pattern

Before an action is applied, you take a snapshot of the current state and save it into an array. That snapshot is the Memento.

If the user wants to undo, you simply pop the last memento and apply it. The program returns to the state it was before the last action was applied.

This pattern is memory intensive; each memento is relatively large since it captures the whole current state.

But it's also the easiest to implement since you don't need to explicitly code all cases and their inverse actions that you need to in the Command Pattern (see below).

const mementos = []
const input = document.querySelector('input')

function saveMemento() {
  mementos.push(input.value)
}

function undo() {
  const lastMemento = mementos.pop()
   
  input.value = lastMemento ? lastMemento : input.value
}
<h4> Type some characters and hit Undo </h4>
<input onkeydown="saveMemento()" value="Hello World"/>
<button onclick="undo()">Undo</button>

The Command Pattern

For each action the user takes, you also save a corresponding inverse action/command. For example, each time you add character in a textbox, you save the inverse function; which is to delete the character at that position.

If the user wants to undo, you pop the last inverse action/command and apply it.

const commands = []
const input = document.querySelector('input')

function saveCommand(e) {
  commands.push({
    // the action is also saved for implementing redo, which
    // is not implemented in this example.
    action: { type: 'add', key: e.key, index: input.selectionStart },
    inverse: { type: 'remove', index: input.selectionStart }
  })
}

function undo() {
  let value = input.value.split('')
  const lastCommand = commands.pop()
 
  if (!lastCommand) return
    
  switch (lastCommand.inverse.type) {
    case 'remove':
      value.splice(lastCommand.inverse.index, 1)
      break;      
  }
  
  input.value = value.join('')
}
<h4> Type some characters and hit Undo </h4>
<input onkeydown="saveCommand(event)" value="Hello World"/>
<button onclick="undo()">Undo</button>

The snippets I've written only work when adding characters, then hitting undo to return to the state before the characters were added so they are an oversimplification on how you should implement this.

Nevertheless I think they demonstrate the core concepts of both patterns.

FWIW I'm using UndoManager in my projects as a stack for my commands.

nicholaswmin
  • 21,686
  • 15
  • 91
  • 167
  • like you wrote i think command pattern are the better choice. Is it possible to make me a little script to see, explain and understand how this should be looking for in js-code? – RainerS Jan 29 '19 at 08:07
  • I'm writing them now, but be advised that they are a gross oversimplification of the formal patterns. – nicholaswmin Jan 29 '19 at 08:09
  • ok - could you give me a example for the command pattern too? so i can start to learn, did this function works after a pagereload? – RainerS Jan 29 '19 at 08:14
  • 8
    Depending on the type of application, a *"Memento of commands"* might be better. For instance in graphic tools, you can't always have an inverse command, and saving each full state would drain the memory in no time. So storing all the commands and calling them all in sequence on undo is the way to go. – Kaiido Jan 29 '19 at 08:32
  • 3
    @Kaiido Stellar, didn't know about that. Basically you're replaying all the commands until you reach the current state - 1, is that correct? – nicholaswmin Jan 29 '19 at 08:33
  • 2
    .Yes that's it. – Kaiido Jan 29 '19 at 08:34
  • "Memento of commands" is a great idea! I would suggest a rename though to avoid confusion - I initially confused it to be a list of commands that would help you undo, step-by-step from present to the past. Instead, I'd just call it "command history", and explain: store your starting position (original image in photoshop), and save each command (edits). To Undo, start from the beginning and replay all commands except 1." – Ben Butterworth Jan 28 '23 at 23:09