Undo Functionality with the Command Pattern in JavaScript
In object-oriented programming, the command pattern is a behavioural design pattern in which an object is used to encapsulate all information needed to perform an action or trigger an event at a later time. This information includes the method name, the object that owns the method and values for the method parameters. - Wikipedia
To summarise the Command pattern is a way of separating methods from classes. The reason to do this is so that commands can be reused and by doing so classes that rely on similar functionality do not depend on each other.
So how does the Command pattern help implement undo functionality? A Command is a set of 2 methods:
- Execute: A function that does the action of the command
- Undo: A function that reverses the action of the command
We can undo all the executions by calling the undo method from each command. Lets look at a simple example:
var num = 1; var command = { execute: ()=> num++, // add num undo: ()=> num-- // sub num }; command.execute(); // 2 command.execute(); // 3 command.execute(); // 4 command.undo(); // 3 command.undo(); // 2 command.undo(); // 1 <-- original value
Breaking it down to core concepts we can see that the command is just an object with an action and a reverse of that action.
A way we can integrate this into classes is by making classes into Invokers. Invokers execute commands and manage the history of the executions.
function NumInvoker(n) { this.data = n; // our changing value this.history = []; // commands executed } NumInvoker.prototype.execute = function(cmd) { this.history.push(cmd); // add to history cmd.execute.call(this); // execute with this === Num }; NumInvoker.prototype.undo = function() { var cmd = this.history.pop(); // get last executed command cmd.undo.call(this); // call undo }; var Add = { execute: function() { this.data += 1; }, undo: function() { this.data -= 1; } }; var three = new NumInvoker(3); three.execute(Add); // 4 three.execute(Add); // 5 three.execute(Add); // 6 three.undo(); three.undo(); three.undo(); // 3
Real World Use Case
We can use the Command pattern to easily keep a track of changes to a input field:
function Invoker() { this.history = []; } Invoker.prototype.execute = function(cmd) { this.history.push(cmd); console.log(cmd); return cmd.execute(this); }; Invoker.prototype.undo = function() { var cmd = this.history.pop(); return cmd.undo(this); }; function TextArea(value) { this.value = value || ""; Invoker.call(this); } TextArea.prototype = Object.create(Invoker.prototype); TextArea.prototype.render = function() { $("input").val(this.value); }; function InputCommand(key, value) { this.key = key; this.value = value; this.cache = ""; } InputCommand.prototype.execute = function(ctx){ this.cache = ctx[this.key]; ctx[this.key] = this.value; }; InputCommand.prototype.undo = function(ctx) { ctx[this.key] = this.cache; }; var txt = new TextArea(); $("input").keyup(function(){ txt.execute(new InputCommand("value", $(this).val())); txt.render(); }); $("#undo").click(function(){ txt.undo(); txt.render(); });
On the input change the TextArea class executes the InputCommand. This sets the current value of the input as the TextArea value and pushes the command to the history. When we call undo we basically set the TextArea value to the last value the command had before execution.
What can I do with the command pattern?
Now that you know how to implement the command pattern you can:
- Decouple similar logic from classes
- Implement a rollback for certain methods
On a final note if you have any questions or improvements to the code or explanations above please comment below so I can make this article more informative to the community.