Backbone.Undo.js
A simple Backbone undo-manager for simple apps
Download and participate
Watch a video tutorial
Watch this video to see how to work with Backbone.Undo.js and what's going on in the background.
Features of Backbone.Undo.js
Drop it in
Unlike undo managers based on the memento pattern Backbone.Undo.js doesn't require you to implement specific functions. Just drop it in and you're ready to go.
Using Backbone-Events
You don't have to store()
or restore()
certain states. Everything works automatically as Backbone.Undo.js uses Backbone-events to detect an undoable action.
Magic Fusion
Backbone.Undo.js has an internal feature called Magic Fusion that detects if events were triggered in one flow. That way it's able to undo and redo all of their actions at once.
Getting started
Backbone.Undo.js depends on Backbone.js which again depends on Underscore.js (or Lo-Dash.js). Make sure to include these two files before you include Backbone.Undo.js:
<script src="underscore.js"></script>
<script src="backbone.js"></script>
<script src="Backbone.Undo.js"></script>
Setting up your undo manager
Now, setting up your UndoManager is a matter of seconds: Just register the objects that should be observed and start their observation by calling startTracking()
or by passing track: true
on instantiation.
var myUndoManager = new Backbone.UndoManager({
register: [app.mainModel, app.mainCollection], // pass an object or an array of objects
track: true // changes will be tracked right away
});
Backbone.Undo.js now listens to the events those objects trigger. Thanks to Backbone-methods like previousAttributes()
it's able to retrieve the objects' data before and after an event was triggered and pushes it to its internal undo stack.
Alternatively you can register your objects and start observing them independently from instantiation.
var myUndoManager = new Backbone.UndoManager;
myUndoManager.register(app.mainModel, app.mainCollection); // Pass any number of arguments
myUndoManager.startTracking(); // Start observation after instantiation
Calling undo()
and redo()
After you've registered your models or collections and started tracking you just need to call undo()
and redo()
to undo and redo certain actions.
Demo: Textarea
The following demo application is a textarea that stores its value in a model every now and then.
Now, we want to provide this textarea with an undo manager (for the purpose of this demo let's ignore the fact that textareas usually have a native undo manager).
The textarea's model is globally accessible at demoTextarea
. All we have to do is register this model and start tracking its changes.
var undoManager = new Backbone.UndoManager({
register: demoTextarea, // The textarea's model is part of the global window-context
track: true // changes should be tracked immediately
});
We call the undo()
- and redo()
-method when the user clicks on the respective button.
$(".undo-button").click(function () {
undoManager.undo();
});
$(".redo-button").click(function () {
undoManager.redo();
});
We're done. Our demo app now has undo- and redo-functionality and we didn't have to modify our model at all. Try it out:
Activate Magic Fusion
Demo: Taglist
The following demo application is a collection of tags. It let's you enter several tags at once by seperating them by comma. An undo manager is installed to undo and redo your changes. However, the behavior is rather unexpected:
If you add several tags to the collection at once you can only undo them separately. This is because adding several models to a collection triggers several "add"
-events which result in several internal undo actions as Backbone.Undo.js creates its undo actions from the events an object triggers.
To solve this problem Backbone.Undo.js is equipped with a mechanism called Magic Fusion. This is an automatic detection procedure that can tell which undo actions belong together to undo and redo all of them at once.
All you have to do to activate Magic Fusion is pass true
when you call undo
or redo
.
undoButton.click(function () {
undoManager.undo(true); // Magic Fusion is activated
})
redoButton.click(function () {
undoManager.redo(true); // Magic Fusion is activated
})
With Magic Fusion the taglist's undo- and redo-functionality behaves just as expected:
Advanced functionality
Backbone.Undo.js generates its undo actions from events the observed objects trigger. It supports the following events:
- change
- add
- remove
- reset
Demo: Planes
The following demo application is a collection of colored planes which can be resized and moved.
When we resize or move a plane and undo this action an unexpected behavior occurs: Even with activated Magic Fusion the undo manager doesn't undo the complete resize or the complete move but only every single step of it, because it doesn't know that all the changes are part of one long, ongoing action.
To solve this problem we need to make sure that only the states before and after a resize or move are recognized so that the steps in between are ignored.
The central event in this case is the "change"
event because it's triggered whenever a plane is resized or moved and its data is turned into undoable actions, called UndoActions, inside Backbone.Undo.js.
The module responsible for creating UndoActions from events is called UndoTypes. There's an UndoType for every type of event. They are the core of Backbone.Undo.js
First, let's remove the UndoType for "change"
:
Backbone.UndoManager.removeUndoType("change");
Now, we can only undo adding new planes. Resizing and moving the planes around won't create any UndoActions:
The application sets the attribute "isChanging"
to true
when a resize or move begins and it sets it back to false
when the action is over. We use this circumstance and create an UndoType specific for this change-event.
Backbone.UndoManager.addUndoType("change:isChanging", {});
An UndoType is an object with the functions "on"
, "undo"
and "redo"
and an optional "condition"
property:
-
The
"on"
function is called when the event the UndoType is registered for gets triggered. It's called with the same arguments as a regular listener in Backbone. To generate an UndoAction that is pushed to the UndoStack it has to return an object with the properties"object"
— the collection or model which triggered the event,"before"
— the data before the event was triggered,"after"
— the data after the event was triggered and optionally"options"
— which can be whatever you need. -
The
"undo"
function is for restoring the"before"
-state. It gets called with the arguments"object"
,"before"
and"after"
and"options"
. -
The
"redo"
function is for restoring the"after"
-state. It gets called with the arguments"object"
,"before"
and"after"
and"options"
. -
The
"condition"
property can be a boolean value or a function and can prevent the creation of an UndoAction by beingfalse
or returningfalse
. If it's a function it's called with the same arguments as the"on"
function.
Backbone.UndoManager.addUndoType("change:isChanging", {
"on": function (model, isChanging, options) {},
"undo": function (model, before, after, options) {},
"redo": function (model, before, after, options) {}
});
The "on"
function is called once at the beginning of a resize or move and once at the end of it, because that's when the isChanging
attribute is changed. When it's called at the beginning, isChanging
is true
, at the end it's set to false
. We store the model's data at the beginning in an external variable and return it at the end together with the other data.
var beforeCache;
Backbone.UndoManager.addUndoType("change:isChanging", {
"on": function (model, isChanging, options) {
if (isChanging) {
// A resize / move has just begun
beforeCache = model.toJSON();
} else {
// The resize / move has ended
// Return this data so that an UndoAction is created from it
return {
"object": model, // The plane's model
"before": beforeCache, // Its data from before the resize / move
"after": model.toJSON() // Its current data / after the action
}
}
},
"undo": function (model, before, after, options) {},
"redo": function (model, before, after, options) {}
});
We now have the data from before and after an action. To undo and redo an action we only need to set the model's attributes to the before
or after
data.
So, the complete code looks like this:
Backbone.UndoManager.removeUndoType("change");
var beforeCache;
Backbone.UndoManager.addUndoType("change:isChanging", {
"on": function (model, isChanging, options) {
if (isChanging) {
beforeCache = model.toJSON();
} else {
return {
"object": model,
"before": beforeCache,
"after": model.toJSON()
}
}
},
"undo": function (model, before, after, options) {
model.set(before);
},
"redo": function (model, before, after, options) {
model.set(after);
}
});
By suspending the regular "change"
-UndoType and adding a new one we modified our undo manager's behavior to meet the user's expectations. The demo application now has the anticipated undo behavior.
The demonstrated way is not the only way of solving this issue. Other solutions are possible, for example using timeouts. Nevertheless, in any case the UndoTypes-API is the way to go.
It's also possible to add an UndoType for a specific instance of Backbone.UndoManager
instead of using the global approach presented here. By modifying the UndoTypes for specific instances you can have several undo managers and adjust their behavior to the events and needs of the objects they observe. With the merge()
-function you can combine them again in one single undo manager to have different, adjusted behavior, but only one main undo manager to call undo()
and redo()
on.
To read more about that and the UndoTypes-API in general click here.
API and documentation
A complete overview of the API is included in the README.md
file in the Github repository. Click here to read it.