Undo Made Easy with Ajax (Part 1)
As users, we make mistakes. As designers, we need to design with mistakes in mind, as I argued in my recent article, Never Use a Warning When You Mean Undo. Undo is the ultimate safety net, lending an incredible sense of solidity to an interface. That’s why every desktop application from Word to Photoshop provides multiple-level Undo.
So, then, why are Web apps that provide any sort of Undo so few and far between? The answer I often get is that Undo is hard to implement. I’m here to tell you that it is not.
In this series of blog posts, my goal is to explain just how easy it is to provide Undo functionality. Recently, I gave a preliminary version of this post in a workshop. After giving the front-facing demo of how Undo could work, the audience moved slightly towards the edge of their seats (it’s all you can hope for in the post-lunch session). When I opened the source code and started showing how I implemented undo, the universal response was, “Why are you bothering to explain this implementation? It’s barely anything at all. We’re software engineers. This is easy.”
That’s my point!
Adding Undo to your interfaces profoundly and positively affects the usability of your site. It reduces user frustration, and increases user trust. Both of those outcomes mean that more users continue coming back, which helps your bottom line. Remember: To the user, the interface is the product.
Now you have no excuse to not implement Undo. So, without further ado, here’s the first implementation method.
Method 1: The Event Queue
The situation: You are creating an online to-do list, and the user needs to be able to delete any item in the list. You don’t want the user to delete an item accidentally, nor do you want them to be unable to change their mind after a delete or two (this second requirement is a stronger version of just being able to recover from the “ohnosecond” I described in my earlier article). The method currently employed across the Web is to warn the user with a dialog box.

Even 37 Signals, normally a bastion of good design, uses warnings instead of undo.
Not only does this warning not work — it also locks up the Web browser, requiring a decision before being able to switch tabs. If you need to check your email in order to figure out whether you can delete a to-do, you are out of luck.
Try out the to-do list with the standard (but not very humane) warning dialog box solution.
Now say that you change your mind about deleting something, or you want to delete 3 items in a row. Sort of frustrating, isn’t it?
The correct solution is Undo, which we can implement here with an event queue and an “onUnload” callback. When the user clicks delete, the to-do item disappears. Normally, at this point we would send an AJAX request to the server to actually delete the item. When writing for Undo, instead of immediately sending the AJAX call, we delay until the user navigates away from the current page (which we detect using the “onUnload” callback). This is achieved by placing a reference to the to-do item in the event queue for safe keeping.
When a user clicks Undo, we pop the last item added to the event queue and make the related to-do item visible again. When the user navigates away from the page, or closes it, the “onUnload” event gets called. It’s at this point that we iterate through the event queue and send the AJAX requests to server-side delete the to-do items.
One of the big benefits of using the event queue method is that it makes multi-level Undo almost trivial. And it also makes deleting a whole bunch of to-do items painless (which is not the case when deletes cause warnings). Try it out!
The Source Code
Intrigued? Want to know more? Good. Here’s the documented source code.
Caveats
The event queue method of implementing Undo isn’t perfect:
- If the user’s browser or computer crashes, then the user’s work will be lost. This is clearly not optimal. On the other hand, this is a rare circumstance and can be safely ignored for all but life-and-death important data.
- Real-time collaboration won’t work with this method of Undo because changes are not sent to the server until the user exits the page. Thus, other users will not see updates in anything approximating real time. Other methods of implementing Undo get around this problem, but that will have to wait until another post in this series.
- It is inadvisable to use this Undo method for sending emails or other time-sensitive actions. Because the action only gets completed when the user exits the page, it might take half an hour or longer before the email actually gets sent. That isn’t acceptable.
- [Update] Reader Alexander Botero-Lowry pointed out that if you deleted items in the to-do list and then opened up the page in a new tab, those items would seem to magically re-appear. To learn how to solve this problem, read Undo Made Easy with Ajax (Part 1.5).
Conclusion
As you have seen, Undo does not have to be a time-consuming undertaking. The event queue method can even be done entirely client-side, which means that you can implement Undo without changing a thing about your back end.
So, no more excuses about Undo being difficult to implement. It’s time for Undo to make its way onto the web!
Next week, I’ll show how to tackle Undo for time-sensitive actions.
RT @azaaza Undo Made Easy with Ajax (Part 1) | Follow @azaaza on Twitter | All blog posts
Varsoil
Those ‘are you sure’ boxes have always bugged me. I am generally not ‘sure’ about anything; it just seemed like the thing to do. So to get things done, I have to lie and say that I am sure. Therefore if things go wrong I’m really set up to take the fall. Not only was I warned, but I lied about being sure.
Michael Latta
For a single user app this may work, for a multi-user app this has real problems. I think I have deleted something, and open another window only to find it is still in the database because the first window has not unloaded. I call a friend to collaborate and they still see the old version of an item because it has not updated yet, but is sitting in the event queue.
I agree UNDO is important even in web apps, but since web apps are generally multi-user the implementation needs to be in the server not the client.
Bryan
Sure, actual browser and computer crashes are less common than they used to be, but with everyone using mobile technologies now, I claim that a loss of connection is far more likely than crashes ever were. From a webserver’s view, the cases are indistinguishable.
I would think you would lose all of the trust you gained from this undo system the first time a user made an update from their phone, lost the connection, then logged in later from their desktop to find the update missing.
Reid
Alas, neither of the above work in Safari uder Mac OS X. :-(
The perils of web development..
Ryan Singer
I’ll add another hole in this approach. Deleting often means more than “don’t show the item”. By deleting something, you may change the state of its container or parent object. For example, when you delete the last item in a list, the list may change state to “Complete” and be displayed differently. Now if you undo the delete, you not only have to show the item again, you also have to revert the change in state on the list.
It’s a nice idea, but problems like this along with the connection issues mentioned above are among the reasons we won’t see this implemented anytime soon. Deleting is just too important to roll dice with.
Alfred Wong
Not very bullet proof.
Alexander Botero-Lowry
An option to consider is that when a delete occurs we set a flag in the database via an XHR that says “mark this non-visable” and then it’s effectively deleted in a multiuser situation, and it allows for a user to undelete it for a certain amount of time (between periodic database sweeps which delete marked items).
Aza Raskin
@ Michael: Agreed. I think I say as much in the caveats section. This is a light-weight undo. If you know you are working in a single user situation, you can use this kind of solution. For instance, this undo would be great for making your web-based RSS feed manager a lot nicer. Or it might make removing spam from your Wordpress installation more forgiving. There are slightly heavier weight solutions (like to one proposed by Alexander) that do solve multi-user problems. If you need that, then go the extra implementation mile.
@ Bryan: Storing an undo queue on the server is definitely a more robust approach. However, losing a connection does not necessarily mean the data would be lost. A simple cookie storage method is enough to solve the problem. But even without that, your argument is for designing to the lowest common denominator. Were the web still following that approach we’d never use AJAX or flash. My answer is degrade nicely (if you are worried about the mobile market force degradation based on user agents) and design for usability.
@ Reid: Thanks for finding that. It is fixed now.
@ Ryan: This is a proof of concept. You may have to do something more clever that simply hiding a to-do item. But we are software engineers, we’re smart enough to tackle this kind of problem.
dgurba
There is a question as to whether or not the task is recoverable at all …
Certain items, like say remove an mp3 from my online audio vault. I dont want the server slowed down by commented out code or having that data merely “deactivated” in a db anywhere … I want the data permanently deleted, the server needs the recovered space. In those cases, an Undo is not possible … and the “Are you sure??” confirmation makes sense.
I really like the idea of undo on the web, just that it only fits certain tasks which can be placed on a stack. “Undo reboot” also hardly makes sense (given a window of opportunity of 30 secs till shutdown … maybe it WOULD be ok :) )
Zephyr
And now to integrate this with everyone’s favorite undo button for the web, the Back button.
Alexander Botero-Lowry
The real problem I see with it is the fact that the undo queue is a FIFO. If I’ve deleted three items, and want the first back, that’s a lot of steps. I haven’t looked at the code much, but if the event queue is just an array, it should be trivial to handle that in a more powerful way.
The other issue I see if a general need for a flush button, especially in a multi-user situation. This is less important if you use the visible bit, but basically I leave tabs open for a long time, so sometimes I want to delete something, but I don’t want to have to close the tab and come back for the delete to go through.
Alexander Botero-Lowry
erm s/FIFO/LIFO/
Sorry about the extra post i couldn’t find an edit…
Andrew Wilson
@dgurba: Users do not think like that. If you are Google and you have the server resources for such a thing, then why should the user care if the resources are freed immediately? They only care that, oops, they just deleted the song they spent 2 days looking for because the album was only distributed in England in the 80s and there were only 500 copies printed. And if your users do care because they have accounts and the accounts are resource limited, then credit their account with the resources. They’ll be pleasantly surprised if they ever need to recover something because of the “oops” factor.
@Alexander: Again, users do not think like that. “flush” is the same thing as “save”: why do I have to do that? As Aza said, this solution is not for multi-user situations, but a flush button is not going to be the answer in those situations.
Alexander Botero-Lowry
Users DO care when they delete an item, to later learn that it wasn’t actually deleted. With that in mind the visible bit is one of the better solutions to this problem. Entirely client-side is going to cause issues even in a single user situation:
[user opens new tab] “Hmm.. why is that TODO item still there, I just removed it…”
Jesper Sjöquist
Interesting conversation!
I would suggest to build upon Alexander Botero-Lowry’s idea about flags. Design your databases for undo functionality by providing a ysnDeleted field for each table with affected data. Then run a “flush”, as Alex mentioned, every 30 seconds or so, simply poll the event queue “is there data to delete? is there data to delete?” Which then flags the items as deleted.
Should the user then want to undo their actions its ridiculously easy to do so.
Depending on what kind of data we’re talking about, a general cleaning function could actually remove the items from the DB at set times every day or even every second day (for those of us who like to keep our browsers running for days on end.. ;)
On a side note, I really digg this blog! :)
[ICR]
The issue raised by Alexander is an important one. While you may be designing a single user environment, on the web there is no way to guarantee that they will not have opened another tab. It seems people frequently have dozens of tabs open, and so will often create a new tab to go to a site because they have forgotten that it is already open and any visual indication of such is lost amongst all the other tabs. Once that happens you then get the “What? I thought I deleted that” situation in the new tab. While it is a good simple example, I think it has little practice use as it stands in this post (though I look forward to the rest of the series).
Chris Pratt
Another case where this fails is when the server is using timed sessions to maintain state. If the user spends too much time cleaning up their todo list, when they finally click the Red X to close the browser, their session may have timed out and the deletions will not be allowed, and there’s no way to warn the user of that fact.
I think the limitations of this method are pretty severe… Ok Aza, what’s the next method so we can start tearing it appart =8^)
(*Chris*)
Alexander Botero-Lowry
I’m probably going to regret this, but I’ve thrown together an example of a server backed undo-todo list. It would be mixed in with some daemon that periodically removes entries if their timestamp is older than some point. The other thing that’s not visible here, but if you’re logged in as a different user it won’t show deleted items, and if you try to request an item by url, it’ll 404 on you. It’s probably still horribly buggy, and wsgiref’s WSGIServer is really slow, but you can check it out at http://www.geekfire.com:8888/todo. I’m happy to share the code if anyone is interested.
AL
Brilliant! Thank you Aza.
Now I have something to back up my usability decisions and convince those developers that it’s not as difficult as they make it seem to be.
Jason Morrison
An elegant and robust way to handle undo is the Command pattern (see “Multi-level undo”). You can even implement the command pattern client-side, and have the undo/redo methods in the Javascript classes perform the asynchronous requests we’re used to writing (especially for a “rich client” type application), although you might want to consider implementing it server-side and persisting the command queue at a session level if you’re interested in graceful degradation.
Jason Morrison
P.S. Nice work! I enjoyed the article very much :) and I definitely agree that undo is underrepresented in web applications.
mike
You said it was easy, but the caveats point out that it’s actually not. For ‘real’ undo, you need to implement functionality at the server.
rAm
Aza,
That was very well laid article and hits the problem on the head.
One should realize where an undo comes handy. an undo is not necessary in all situations.
I don’t even see the “do you want to proceed” kind of pop ups as intrusive, if they really fit the need and are necessary.
Having the technology, its upto us to make use of it in a better manner.
Gregg Bolinger
Interesting article. The only thing I’ll say at this point is the argument that a confirmation box is not “humane”. Granted, the standard javascript browser confirm() is not optimal. That’s why I never use them. Instead I use something like Prototype-Window (http://prototype-window.xilinus.com/index.html) to display alerts and confirmations. This way, I am allowed to switch tabs, etc with no problems.
Since this article did contain caveats I’ll keep my other comments at bay until I see the other solutions to come.
kthejoker
If an action is undoable on the frontend, it should be undoable on the backend.
A better solution would be something where it loads it in the event queue but does not perform the backend work until, say, 30 seconds. I’d bet most undos are clicked within 30 seconds of the original action.
After that time, the backend action should be committed, but the event should remain in the queue. If Undo is clicked after that, there ought to be a specific backend action to “undo” that same event.
And while that may take more work, it’s a lot less guesswork by the enduser on if and when their changes are being recorded, and when they’re just being teased by a false GUI.
nicolash
disclaimer : offtopic
in your blog if i click ” Add yours below.” for comments.
- i jump to “comments” (same line as link -optically) and not to the comment-form
- the link is not understandable in it self (accessibility), consider adding a title.
furthermore
the submit-button has a accesskey of “s”
- as far as i remember “s” should be used for “Skip-navigation”
…by the way accesskey of “9″ is for feedback/comments-forms
…. i love the articles of this blog, but i wouldn’t expect this tiny flaws here – so i couldn’t resists… ;-)
Joshua Bloom
Remember the milk.com has had a great undo feature built in for some time.
I remember reading one of their blog entries about how they built it. You can also review their API for an idea of the server implementation. It’s been very solid for me and I recommend checking out their implementation details if you’re going to build something similar.
Lokesh Dhakar
My proposed solution:
User Clicks Delete
- Item is deleted ( the item’s ‘deleted’ property in DB is toggled to true)
- Item removed from UI
- Undo button appears
User clicks Undo
- Item is ‘undeleted’ ( the item’s ‘deleted’ property in DB is toggled to false)
- Item added back to UI
Summary:
Rather than queueing actions for execution you execute on demand. In the undo queue you add a query that will undo the last action.
This pattern would work in more complex cases as well. For example, user edit’s the name and description of an item and clicks save. You grab the previous values and build a query that will reset the item’s name and description and store it in the undo queue.
-Lokesh
Original Sin
I wholeheartedly agree with the article writer. Undo is a must, and I’m sure it will become standard fare in most web apps as the industry matures. Implementation of unto should be kept in mind as early as when you’re designing your database tables, or else you will have to resort to what are essentially hacks, like client-side queuing (above), special undo tables that keep data that has been deleted or a similar kludge.
From a server-side database perspective, there are two types of undo. A simple deleted-flag per record, which can be raised or lowered by the user at will (but presented as delete/undo), or a second, incremental undo that creates a new record for each change (similar to a Wikipedia history), where a user can roll-back to previous states. The first type is relatively straight-forward to code. The second requires a little fancy SQL so that the user is always presented with the latest versions of things, but is still relatively straight forward.
A reader mentioned situations where unless something is truly deleted something else cannot happen (for instance storing MP3s in a limited space), bar situations like that, you very rarely need to delete data right away. It is also very useful to see what your users delete, it is an excellent resource for identifying bad design.
The challenge I see with implementing undo is what happens if one action triggers another, and the initial action is undone? How should chain reactions be rolled back if a user undoes the trigger? I would love to hear from others who have been faced with designing for these situations.
Chiru
Hi Aza, I really like this idea. I talked to you at the Rich web experience conference in San Jose. Wanted to share my opinion on some issues people shared:
1. first of all it is about giving the users a great experience. We are trying to make the webapp as versatile as a desktop app in some sense.
2. I like the server side solution. Yes it is more robust, but If we have that we are basically killing the servers. The back and forth using XHR can swamp the server. Also since each request is independant, the overhead on the backend will be high
3. About the undo in a LIFO, we dont have to worry about it too much. Just in MS word, we only undo the last action, not something that was done before that. Once that is undone, we undo the one before that.. it is quiet simple.
4. finally I would suggest something like auto-save. This way the onus is not on the user to “commit” all actions.
Finally, I think this “undo” maynot apply to every webapp out there, so we as s/w developers should pick and choose wisely.
amar
nice idea
can you please put the source code of the first example that “Try out the to-do list with the standard warning dialog box solution.”
thanks ..
Jon Cram
Of the critical comments, most relate to the technique shown here whereas I’d like to focus a little on the interface and how this works against, not for, this technique.
Firstly, I know it’s just an example/proof of concept, however I find the interface shown here to be misleading enough that a little something needs saying. And some people will use the given example too directly (“I like it, I’ll use it”) without thinking things through fully.
For me, I have a slight problem with ‘delete’ next to each list item and a ‘Done’ button. I think more accurate terms need to be used so as to avoid user confusion.
Through the client interface we’re modifying the list contents and, when happy, making the changes permanent.
We’re not deleting items from the list and then being ‘done’ with what we’ve accomplished.
So let’s rename the per-item ‘delete’ to ‘remove’ and the ‘Done’ button to ‘Save’ (or ‘Store’).
This relates to what we’re actually doing, as opposed to describing what we’re sort-of pretending to be doing.
The difference?
‘Done’ means what? So I’ve deleted some items from the list. Why do I need to be ‘done’? I’ve deleted them – that’s it – I’m already done.
Less expectation will be put on changes being real. At present, users could easily expect the deletion of an item to propagate across tabs and so on, as mentioned by others. This can be dealt with, but in dealing with it we’re merely covering our tracks from having partially deceived the user.
So with ‘remove’ and ‘Save’ what do we have? A more exact representation of what we’re doing. I remove items from the list at will and don’t necessarily feel it’s a permanent change.
With a ‘Save’ button, it is more clear still that I have to perform an action to make my changes permanent – with a ‘Done’ button, the user has to make the connection – and I’d not really be expecting unsaved changes to propagate across to anything else.
Example relating to how the current list behaviour can be confusing:
Two users both viewing the same shared nework folder. User A deletes a file and expects it to be no longer present in user B’s view without needing to further commit the action. User A gets confused when user B’s view remains unchanged.
Example relating to how ‘remove’ doesn’t include the expectations of ‘delete’:
Two users again viewing a shared network folder. User A begins renaming a file. User A doesn’t expect these changes to propagate across to user B’s view until committing the changes – clicking away from the file name, pressing ‘enter’ etc. User A realises their change to be non-permanent and expects the need to commit before seeing the change reflected elsewhere.
And to make this criticism constructive: use the correct terms for what is happening, give the user the correct expectations (relating to what the technique is actually doing, not pretending to be doing) and we no longer face some of the problems with this technique upon which people have commented.
Ruben Daniels
For a robust working undo/redo solution you will need an extensive framework that will monitor actions and ‘knows’ how to undo them. Here’s an example of such an implementation:
http://www.ajax.org/?demo=platform/smartbinding
Its build on Javeline PlatForm and has support to do/undo the state sync with the server, per user action, when the server supports it (think of undo remove). In that way it is possible to build a collaborative solution. On top of that when the application crashes the server still has a record of the users work.
jest Staffel
This is a very good script.
can you please put the source code of the first example that “Try out the to-do list with the standard warning dialog box solution.”
i have a example on my site:
http://scripts.ajaxflakes.com/category/accordion/
karuna
Very nice article!
Probably in your coming blog entries, you would be discussing about implementing Undo server side. Could you also list out when it would be appropriate to have Undo implemented on client side and/or server side??
Craig Fitzpatrick
I see a lot of “workaround” ideas for Undo, but haven’t seen a true (desktop equivalent) design pattern emerge yet. I really hope we don’t get to the point where users on the Web have to think about what “kind” of Undo an application implements (flushing a queue like a Save, or only commiting on UnLoad), because there’s already a universally understood version of Undo on the desktop: your document state is reverted to exactly what it was before you did the action, but yet, your actions are still committed immediately.
Client/server development and multi-user issues certainly make Undo implementation a lot more complex, but I think the only TRUE Undo is one that keeps copies of data state after each action (at least until the end of the session, when the intermediate states can be flushed), similar to version control, that can be rolled back and forward.
Any workaround Undo I’ve seen so far severely limits the wonderful collaborative nature of the Web, especially even for some of the example applications, where we could be talking over Skype while both looking at a to-do list and seeing each other’s changes as we do them by refreshing our browser.
I really don’t think there’s a quick fix for this important problem. Versioning the data’s state as people perform actions is the only way to go.
Real Estate Postcards
Great job…read your article on A-List part. Wonderfully stated
Zayıflama Lida Fx15 Ve Biber Hapı Zlfvbh
For a single user app this may work, for a multi-user app this has real problems. I think I have deleted something, and open another window only to find it is still in the database because the first window has not unloaded. I call a friend to collaborate and they still see the old version of an item because it has not updated yet, but is sitting in the event queue.
porno
Any workaround Undo I’ve seen so far severely limits the wonderful collaborative nature of the Web, especially even for some of the example applications, where we could be talking over Skype while both looking at a to-do list and seeing each other’s changes as we do them by refreshing our browser.
Sex
Those ‘are you sure’ boxes have always bugged me. I am generally not ‘sure’ about anything; it just seemed like the thing to do. So to get things done, I have to lie and say that I am sure. Therefore if things go wrong I’m really set up to take the fall. Not only was I warned, but I lied about being sure.