Tip:
Highlight text to annotate it
X
Hi, I'm Oliver and I'm the creator of Backbone.Undo.js and
I'm gonna show you how it works and what's so cool about it.
Backbone.Undo.js is a very simple undo manager for Backbone.js.
And the cool thing is that you don't really have to change your app usually at all [to make it work] and
you just need a few lines of code and you're done.
So, how does that work? The principle of Backbone.Undo.js is that
you register the objects whose changes you want to undo and redo and the undo manager
then observes these objects and listens to the events they trigger.
On every event it creates a before- and after-state and stores it internally as an "UndoAction"
in an "UndoStack".
So, this is how Backbone.Undo.js works and as you can see the major advantage is that
it makes extensive use of Backbone's own mechanisms — so, events, functions like previousAttributes()
to get the before-state, etc. — and that way it's able to do a lot on its own which
is the reason why it's so simple.
So, let's try it out then. You probably know the site TodoMVC.com. This
is a site that presents a whole range of JavaScript-based MVC-frameworks with to-do-lists as sample
applications. And of course it also has a sample application for Backbone.js.
So, I'm loading the source code down, unzip it,
the Backbone-to-do-application is [in] "architecture-examples" > "backbone".
Now, when we open the index.html file you can see, it's the application.
We can write to-dos in here. "Buy food for the cat"
"Don't forget the dog" We can change them
"and a cracker for Polly" Mark them as done.
And remove them from the list.
So far, there's no undo functionality. There's no undo button and if we press 'control Z'
Nothing happens. That's what we're going to change.
I'm inserting Backbone.Undo.js and a Keybinding-manager in the directory.
This is a Keybinding-manager I have written, jQuery.Shortcut.js, but you can use any [other] one
— or none at all if you don't wanna use shortcuts — so, this is not a dependency,
and yeah, it's all up to you.
I'm gonna edit the index file now. I insert Backbone.Undo.js
and jQuery.Shortcut.js
To create an undo manager we have to instantiate Backbone.UndoManager
So var undoManager = new Backbone.UndoManager();
We could pass arguments to the constructor, but for now we're not doing that
we get to that later in the video.
If you take a deeper look at the to-do-application you can see that all the required objects
are properties of the "app"-object. This is globally accessible. And in this
"app"-namespace we have the to-dos object. This is where the data is. This is not a constructor
function unlike all the other properties of the "app"-namespace, this is the actual
list. And this is what we need.
We're registering this list with our undo manager.
Now, Backbone.Undo.js won't observe the object yet. We have to explicitly tell it that
the object is ready to be observed. And we're doing that by calling
undoManager.startTracking();
So, from here on changes are tracked. Now, we only need to bind that to our
shortcuts. So, I'm gonna do
jQuery.Shortcut.on
"command + Z", "meta + Z"
undoManager.undo();
And on "meta + shift + Z"
undoManager.redo();
Let's save that. That's it, really.
Now, let's try that out and create some to-dos again.
I'm gonna do the same thing I just did. So, first to-do
"Buy food for the cat" "Don't forget the dog"
And not only buy food for the cat, but "a cracker for Polly", the budgie.
And we can mark them as done.
And we can remove them from the list.
And now I hit command Z,
command Z, command Z,
command Z, and command Z.
So, undo works. Now, let's redo some changes by hitting
command shift Z, command shift Z,
command shift Z.
You may have noticed that when we were undoing the removal of the to-dos they were inserted
in the wrong order. Now, this is because the AppView just appends new to-dos to the
list and doesn't keep track of the actual index where in the list they should be placed.
So, there's potential to tweak the app here and there to improve how it works together
with the undo manager, but the bottom line is that we implemented an undo manager in
just a few lines of code! Here we have them, three lines of code.
And if I hadn't talked so much, in just a few seconds!
And we didn't even touch the original code. So, I think that's pretty cool. And this is
why I call Backbone.Undo.js an extremely simple undo manager.
Let's get to the next demo. This is a textarea that stores its content
in a Backbone-model every now and then — roughly after every word.
Now, you might think, wait a second! Textareas already have an undo manager, a native one!
That's true, we just ignore that for this demonstration.
So. The model the textarea saves its content to is globally accessible again, of course.
And it's a property of the window-object called
demoTextarea
So, now let's implement an undo manager for that.
Again, we instantiate Backbone.UndoManager.
And I just said that you can pass an argument to the constructor function.
If you already have the objects you want to register with the undo manager you don't have
to call the register function, you can pass them directly on instantiation.
You pass an object-literal with the attribute "register"
and we assign it to our "demoTextarea". Now, if you have more than one object to register
you can also pass an array of objects. And if you don't have to modify these objects
any more, if they're already set up and ready to be tracked, then you can just pass
track: true So, this is the shorthand way. We don't have
to call the register() and the startTracking() function anymore. We're doing this on instantiation.
So this is even shorter than what I've just showed you with the to-do app.
Now, we need to call the undo and redo functions somehow. There are already undo-
and redo-buttons in the site and I'm just gonna register a click listener on these two
buttons.
So, when the undo-button is clicked the undo()-function should be called and with the redo-button,
the redo()-function should be called.
So, let's check that out. Go to our demo, refresh it
and let's type something
"Hello world" "How are you today?"
"Get back to me." "Hugs and kisses."
No. "Please, get back to me."
Now, I click the undo button and you can see what happens. And the same thing with redo.
It just works, it's really simple.
However, you might run into some complications and this is what I wanna show you with the
next demo.
This is a taglist. You can write a tag down below here.
"This is a tag"
Hit Enter and it's added to the list. I've already implemented an undo manager here.
So, we can call undo and redo
and it's gonna remove the tag and add it again back to the list.
So far, so good. Now, we can also add several tags to the list
at once. Let's do that.
"These, are, multiple, tags"
They are added to the list. Now, if I want to undo that —
this happens. They were added to the list at once, but I
can't remove all of them at once.
Now, why is that?
As I just said, Backbone.Undo.js creates UndoActions from events, that's its principle.
When you add several models to a collection several events are triggered and your undo
manager creates an UndoAction from every single one.
This is the reason why it behaves this way. Why it's undoing not all at once, but each for itself.
Now, this is not just a side issue. This happens
not just with collections. You can have that in any app. The user clicks something and
the app doesn't just change one thing, but several things. You can't expect the user
to undo several actions when in fact he just did one thing. That's not intuitive or logical.
So, that's why Backbone.Undo.js comes with a feature called *Magic Fusion* and what it
does is this. It automatically detects which UndoActions
belong together and is able to undo and redo all of them at once.
Now, the word "magic" might scare you off a little, but it's really, really careful
in what it assumes belongs together and if you dig into the code, you'll see how it works.
You're gonna realize that it's solid and usable. However, it's still an opt-in
and not turned on by default.
To use Magic Fusion you just have to pass true to the undo and redo function, it's just
as simple as everything else.
Let's try that out again. I hit refresh and I'm gonna add
"Several, tags, at, once" again and
"Other, tags, at, once"
Now, let's undo this and voilà — Magic Fusion has figured out which actions
to undo together.
As a rule of thumb you should just always activate Magic Fusion when you bind the undo
or redo function to something the user triggers — so, buttons or shortcuts or
anything like that.
Now, before I show you the advanced functionality I wanna give you a quick background story.
I developed Backbone.Undo.js for an application I created for my bachelor thesis.
This is some kind of Keynote-ish, Photoshop-ish or InDesign-ish application. It's a document
editor basically and it's webbased of course, so it runs in a webbrowser. It's also
based on Backbone.js. When I developed this application I was looking
for an undo manager I could easily implement. And I didn't really want to implement it deeply
into the code, because I wanted loose coupling and everything, obviously.
I knew Backbone and I knew that it had those methods like previousAttributes() and
so on, so I had this principle in mind of an undo manager that makes use of this
so that you don't really have to add much code at all, but I was unable to find one.
There were either just general JavaScript undo managers, so not particularly made for
Backbone, or two undo managers made for Backbone, but they were both based on the memento-pattern.
And so I was like ohkay... well, then... I'm gonna do it myself — and that's how
Backbone.Undo.js came about. So, I used it in my application, as I just
said. And as you might tell, this is not a simple application. This is quite the rich
web application. So, during development I realized that a memento-based,
undo manager might be more powerful. However, I stuck to my Backbone.Undo.js and extended
its capabilities and eventually it worked. The undo manager in this application is powered
by Backbone.Undo.js. So, you can see it is possible.
But — if you have a large-scale web application and you're currently looking for an undo manager
you might rather take an undo manager based on the memento-pattern.
Backbone.Undo.js is extremely simple, but it's also rather suitable for simple applications.
You can try it out, because as you could see, it's very easy to implement, but it might
not suit your needs.
So, that said, now I'm gonna show you the advanced functionality I implemented to make
it work for my document editor.
As I've already said, the UndoActions that are stored in the UndoStack are created from
events. This sounds like a generic job, like some
transformation of events into UndoActions, but this is of course not the case. You can't
apply the same function you use for a "change" event to a "reset" or "add" event. They
get totally different arguments and they serve different purposes.
So, internally Backbone.Undo.js has different functions for different types of events. And
those are called UndoTypes.
Let's have a look at how an UndoType is structured. Every UndoType needs to have three functions:
"on", "undo" and "redo". The "on" function is called when the event
is triggered and it's responsible for retrieving and returning the data we need
to undo and redo the action. If the "on" function doesn't return the necessary data, the undo
manager won't create an UndoAction. The "undo" function is of course for undoing
the action, so for restoring the original state.
And the "redo" function is for reversing that and restoring the new state.
Let's have an example. Let's implement the UndoType for "reset".
The "reset" function gets the collection and an options-object as arguments.
We must return an object with the necessary data.
And that is: the object which caused the event, the before-state, the after-state and optionally
an options-attribute and, yeah, we don't need to return any options, but we do it
in this example.
So, the object that triggered the event is the collection.
We can retrieve our before-state from the options object, because there we have
options.previousModels Our after-state is the list of current models.
Now, make sure not to just return a reference, but a copy of it. So, I'm cloning it here.
As options we just pass our options-object.
The "undo" function is called with object, before, after and options. So, all the data
we returned in the "on" function. Our object is of course our collection.
So, let's rename that. To undo a reset, we just need to replace the
current models with the previous models.
The "redo" function is called with the same arguments. Again, the object argument is our
collection. And here we just do the opposite and reset
the collection with the models from the after-state.
Now, optionally you can set a "condition" property that decides whether the "on" function
gets called at all. It's always true by default. You can set it to false if you want to prevent
UndoActions from getting created. You can also set this to a function.
This function gets the same arguments like the "on" function. So, it can check the arguments
and then decide if an UndoAction should be generated or not.
Now, you might already have noticed it: This is it. This is where the magic happens. These
UndoTypes are the very heart of Backbone.Undo.js. So, while developing my application I realized
I needed access to these UndoTypes.
And that's what I wanna show you in the third and last demonstration.
This is an app where you have colored planes and you can move them around,
resize them and add new planes
and do the same thing with them.
Let's implement an undo manager for that.
So, again, we instantiate our undo manager.
We start tracking right away — track: true And we register "demoEditor", our collection.
By the way: You just need to register a collection, not every single model
of it, because we just need the events and the events the models trigger are delegated
by the collection. So yeah, that's totally sufficient.
We bind our undo()- and redo()-function to our undo- and redo-buttons. So,
undo-button click() calls undoManager.undo() and redo-button click() calls undoManager.redo().
Of course, we activate Magic Fusion again.
And now, let's see what happens.
I'm dragging some planes around and I'm resizing them
and I add one. And I undo this — this works. And I undo
that and... what's that? Nothing happens? Okay, so, you can see it looks like slow motion
or something. The undo manager doesn't undo the entire drag, but every single step of it.
This is of cource because your undo manager doesn't know that all these changes are part
of a long, on-going action. And even Magic Fusion can't help us with this
because it's just undoing what happens at once and these changes don't happen at once,
they happen one after another with a few milliseconds in between.
So, what can we do about that? That's where those UndoTypes come into play.
What we expect when we hit undo and redo is that it undoes and redoes the complete move
or the complete resize at once. Not step after step.
So, when I begin to move a plane, that's when it should grab the data and store
it as the before-state. And when I end the move
that's when it should create the after-state. And this is when the UndoAction should be
created, so that it has the before- and after-state.
So, how do we do that? We could use the "condition"-property here.
Let's set it to false for now, to see what happens.
So, this is
Backbone.UndoManager.changeUndoType()
And this is the function. The UndoType we want to change is the one of the "change"
event and we set its condition property to false.
Now, what's going to happen?
Nothing. The moves and resizes are model-changes, so they trigger the "change" event, we've just deactivated the UndoType for "change"
so we can't undo these changes. However we can still undo adding new planes
to the collection. Because that is carried out by the UndoType
for the "add" event and we didn't modify that one.
So, that's something! Now, we could use a function as our condition property and somehow
figure out when an action starts and when it ends and only return true then and false
in between or something. So, we could do it like that.
But I'm gonna take a different approach here. Let's remove the "change"-UndoType altogether.
So, instead of "changeUndoType" I say "removeUndoType" and we don't need that.
Instead, I'm adding a new one. The models of these planes have an attribute
called "isChanging" and that is set to true during a move and resize. And it's false
otherwise. So whenever this attribute is changed,
a change event is triggered for it and I'm just going to log that here.
"v" is the value of "isChanging". So, let's have a look.
It's only triggered at the beginning and end of an action, because that's when the value changes
We're gonna use that! We add a new UndoType
for this specific event.
And of course we have to implement the functions again. So, there we have the
"on" function.
It gets the arguments that are triggered when an attribute-change happens, so that is
model and the value of isChanging. Now, when an action begins, isChanging is
true. So, here we are when an action begins.
We need to get the models data and store it. So, let's create a new variable, beforeState,
outside this scope. And we store the models attributes in here.
model.toJSON(); Now, when an action ends, isChanging is false.
So, that's else then. And this is where we want to get an UndoAction
created, so here we return an object with the properties:
object — which is our model, the before-state — which is our beforeState
and the after-state which is the model's current data.
We don't need to add the "options" property. Now let's implement the "undo" function.
It gets the model, the before-state and the after-state as arguments.
We don't need the options-argument that's why I'm leaving that out.
All we have to do now is set the model to the before-state.
The "redo" function works quite similarly. It gets the same arguments.
And here we set the model to the after state. And we're done.
So, let's try it out.
Let's undo that. And it works just like we expected it to work!
Now, the functions we used to add, change and remove the UndoTypes were global
functions. We called it on the Backbone.UndoManager-object, not the undoManager-instance.
However, you can use the same API on an instance and it won't have global effects.
Now, why would you wanna do that?
Let's get back to this document editor again. As I said this isn't a simple app. The
undo manager here doesn't just observe one object, like in all the examples I showed
you, it overserves several ones. When I began modifying the UndoTypes to adapt
them to specific needs of particular objects, they had of course effects on the
other objects as well. So, I was fiddling around, trying to solve
that by checking which object is currently the one that triggered the event and how to
behave then. And I had a lot of distinctions to make.
And guess what, I realized this won't work in the long run. This is just not maintainable.
And so I came up with another solution: Multiple undo managers.
You have an undo manager for just one or two objects and you create special UndoTypes only
for that undo manager. That's why you can use the UndoTypes-API
on a specific instance.
The thing is: With multiple undo managers you have multiple UndoStacks. So, where should
you call undo() and redo() on, right? We need these undo managers to write on only
one stack. And this is what the merge() function is for.
You can merge your special undo managers into a main undo manager and they will add the
UndoActions they create to the stack of the main undo manager.
So, that's how you can keep them apart from each other and that solves this fiddling around.
So, these are the advanced features. This is how you can make it work for larger applications.
And once you've understood the concept of UndoTypes it's actually pretty simple again.
And with creating several undo managers and merging them into one you get a lot
of power.
So, the bottom line is Backbone.Undo.js is extremely simple and very easy to set up.
And it's also quite capable if you use the advaned functionality.
That's it. That was the extensive introduction to Backbone.Undo.js. This was a complete overview
over all the features, so you're ready to use it now.
You can try out the demos yourself on backbone.undojs.com
or just undojs.com. You'll be redirected. There's also the sourcecode to all the demos.
And everything I explained in this video is written here, too.
You can download the file with and without comments and you got a link to the
Github repository here.
In the github repository you have a complete overview of the API.
So, check that out. I hope Backbone.Undo.js can help you. It's
at least what I wanted when I was looking for a Backbone-based undo manager.
However, it's still rather young. So, you may find bugs. But you're welcome to help
developing it, of course.
So, that's it. Try it out. Thanks for listening, bye!