Skip to content

Instantly share code, notes, and snippets.

@kimhogeling
Created November 3, 2015 19:05
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save kimhogeling/b646b432ba974f31c6e4 to your computer and use it in GitHub Desktop.
Save kimhogeling/b646b432ba974f31c6e4 to your computer and use it in GitHub Desktop.
Undo/Redo with command pattern in javascript
/**
* The command supertype, which is inherited by subtype commands.
* @constructor
* @param {type} normalAction The normal action.
* @param {type} undoAction The opposite of the normal action.
* @param {type} actionValue The value, which will get passed to the action.
* @returns {Command}
*/
function Command(normalAction, undoAction, actionValue) {
this.execute = normalAction;
this.undo = undoAction;
this.value = actionValue;
}
/**
* Class for creating brave mountain climbers.
* @constructor
* @param {number} start The starting height.
* @returns {Mountaineer}
*/
function Mountaineer(start) {
// The achieved height.
this._currentProgress = start || 0;
// Contains instances of inherited Commands.
this._commandsList = [];
// We do not push and pop commands,
// instead we keep them all and remember the current.
// It start with -1, because this._commands is zero based.
this._currentCommand = -1;
}
// Add the methods for mountaineers
Mountaineer.prototype = {
constructor: Mountaineer,
/**
* Execute a new command.
* @param {type} command Instance of a command.
* @returns {undefined}
*/
execute: function (command) {
this._currentProgress = command.execute(this._currentProgress);
this._currentCommand++;
this._commandsList[this._currentCommand] = command;
if (this._commandsList[this._currentCommand + 1]) {
this._commandsList.splice(this._currentCommand + 1);
}
},
/**
* Undo the current command.
* @returns {undefined}
*/
undo: function () {
var cmd = this._commandsList[this._currentCommand];
if (!cmd) {
console.error('Nothing to undo');
return;
}
this._currentProgress = cmd.undo(this._currentProgress);
this._currentCommand--;
},
/**
* Redo the undone command.
* @returns {undefined}
*/
redo: function () {
var cmd = this._commandsList[this._currentCommand + 1];
if (!cmd) {
console.error('Nothing to redo');
return;
}
this._currentProgress = cmd.execute(this._currentProgress);
this._currentCommand++;
},
/**
* Simple getter of the current progress i.e. the achieved height.
* @returns {Number}
*/
getCurrentProgress: function () {
return this._currentProgress;
}
};
/**
* Climb up action. One of the actions which will be used in the commands.
* @param {type} actionValue Amount of climbed progress.
* @returns {climb.value}
*/
function climbUp(actionValue) {
// `this` is the instance of a command.
return actionValue + this.value;
}
/**
* Fall down action. One of the actions which will be used in the commands.
* @param {type} actionValue Amount of falling regression.
* @returns {fall.value}
*/
function fallDown(actionValue) {
// `this` is the instance of a command.
return actionValue - this.value;
}
/**
* Command subtype for climbing up.
* @constructor
* @extends Command
* @param {type} value The value, which will be passed to the action.
* @returns {undefined}
*/
function CommandClimbUp(value) {
// Constructor stealing for inheritance.
// The order of climb and fall defines which is the normal vs undo action.
Command.call(this, climbUp, fallDown, value);
}
// Prototype chaining for inheritance.
CommandClimbUp.prototype = Object.create(Command.prototype);
/**
* Command subtype for falling down.
* @constructor
* @extends Command
* @param {type} value The value, which will be passed to the action.
* @returns {undefined}
*/
function CommandFallDown(value) {
// Constructor stealing for inheritance.
// The order of climb and fall defines which is the normal vs undo action.
Command.call(this, fallDown, climbUp, value);
}
// Prototype chaining of inheritance.
CommandFallDown.prototype = Object.create(Command.prototype);
// Lots of other commands e.g. jumping, eating, dancing, planking can be added here
// That is all. It is really that simple! Let us see it in action.
var reinhold = new Mountaineer(); // start at 0m
reinhold.execute(new ClimbUpCommand(1)); // climb 1m. (progress = 1m)
reinhold.undo(); // fall down 1m. (progress = 0m)
reinhold.execute(new ClimbUpCommand(2)); // climb 2m. (progress = 2m)
reinhold.undo(); // fall down 2m. (progress = 0m)
reinhold.execute(new FallDownCommand(3)); // fall 3m. (progress = -3m)
reinhold.undo(); // climb up 3m. (progress = 0m)
reinhold.execute(new ClimbUpCommand(4)); // climb 4m. (progress = 4m)
reinhold.execute(new FallDownCommand(5)); // fall 5m. (progress = -1m)
reinhold.execute(new ClimbUpCommand(6)); // climb 6m. (progress = 0m)
reinhold.undo(); // fall 6m. (progress = -1m)
reinhold.undo(); // climb 5m. (progress = 4m)
reinhold.undo(); // fall 4m. (progress = 0m)
reinhold.undo(); // Do nothing, because there are no actions left to undo
reinhold.redo(); // climb 4m. (progress = 4m)
reinhold.redo(); // fall 5m. (progress = -1m)
reinhold.redo(); // climb 6m. (progress = 5m)
reinhold.redo(); // Do nothing, because there are no actions left to redo
reinhold.undo(); // fall 6m. (progress = -1m)
reinhold.redo(); // climb 6m. (progress = 5m)
reinhold.execute(new ClimbUpCommand(7)); // climb 7m. (progress = 12m)
reinhold.getCurrentProgress(); // 12m
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment