Because we all make mistakes
I designed co.mments, so it’s hard to accidentally delete conversations. I did what other Web developers do and added a confirmation popup. It looked good on paper.
Most times, I really do mean to delete stuff, and confirmation popups just get in the way. When I’m scattered, usually early morning before the coffee kicks in, I end up clicking (and confirming) the wrong stuff. If I’m not paying attention to what I delete, I can’t find it again. Especially when I’m deleting something I added a few days back.
Software that forces you to think twice before you do anything is software you can’t trust. And software you can’t trust isn’t fun to use.
So I got rid of the confirmation popup and added an undo button.

How it works
It’s surprisingly easy to do. Now I wonder why I didn’t do it before. I’d like to see that in all my favorite apps, so I’m going to share the recipe and code with you. If you’re building the next best Web app, please add this. Or get your favorite geek to add it to their site.
Each action you do has a URL with all the parameters. I created an undo stack that stores these URLs in the user’s session. Each time you perform an action, the server pushes another undo action URL to the stack. If you just created a new post, the server pushes a ‘delete that post’ URL to the stack.
An undo button at the top of the page uses the most recent undo action URL. Click on the button and it will perform the ‘delete that post’ action. It also removes that action from the stack, so you can undo the previous action. For co.mments, I set the undo stack to five actions, so you can undo more than one change, but it doesn’t take over the database.
Since you can perform an action directly or by undoing a previous button, the undo URL includes the parameter undo=true.
Example using Rails
Here’s the Rails controller with a create action that creates a new record, a delete action that will undo it, and a before_filter that removes undo actions from the stack:
# Remove undo action from stack.
before_filter do |controller|
controller.undo.pop(controller.params) if controller.params[:undo]
end
# The create action creates a new record, the undo action deletes it.
def create()
# Do something useful here
record = Record.create(@params)
# If this is not an undo, add an undo action to delete the record.
unless @params[:undo]
undo.push ("Delete newly created record",
:action=>"delete", :id=>record.id)
end
# Render page (see below).
. . .
end
def delete()
. . .
end
A third method renders the undo form and button, using the last undo action from the stack. The view creates a wrapper element and renders an action:
<div id="undo"><%= undo.render %></div>
For AJAX requests, I use RJS to instruct the browser to update the undo button without reloading:
render :update do |page| page["undo"].replace_html undo.render end
Get the code
I wrapped the code as a Rails plugin. To install the plugin into your Rails app:
./script/plugin source http://labnotes.org/svn/public/ruby/rails_plugins/ ./script/plugin install undo_helper
You can find the source code here. You can also try it out on the co.mments server.
Update: I forgot to mention that GMail has had undo for a while, although only for the last action. Hans posted a screen shot of undo on Technorati.
Pingback: Anonymous
Pingback: hans.gerwitz
Hmm, GET requests are not supposed to have any side effects. Your app will break horribly with things like web accelerators. But I’m sure your method should be relatively easy to fix — just serialise the POST parameters instead of the URL.
@Luke
You’re right about GET, it should not be used for requests that have side effect. This code uses POST, while passing the data in the query string.
Pingback: undo_helper: Add ‘Undo’ to your Rails application
Is it possible to undo a delete or an update action ?
Johan,
I do that in co.mments. If you delete or update, it stores a POST request to create again, or update to the old value.
I need to remove pop up blockers
This is a big improvement in terms of user experience. I wonder, though, how you cope with deleting a record which in turn cascade-deletes many dependent records too?
I suppose you would have to be able to push a group of creation actions onto the stack as a single unit. Does that sound right?
Andy, that’s one possibility.
Another possibility, mark items as deleted an unmark when undone (what I’m using in one of my projects). That would also work better for large objects.
If the objects are not particularly larger but are complicated, you can also serialize them as blobs into an undo table, which can aggregate undos for several types of objects.
Pingback: Labnotes » Rounded Corners - 128 (lolcathost)
Pingback: Contrast | The Blog | 37signals’ biggest flaw