How to write a simple undo system for your app

I really like undos. If I could undo that that last beer… Unfortunately, I can’t. But, I can offer undo to users of my application.

When I thought about undo before, I thought about a complicated Rails plugin that would keep an anonymous ID linking to any table in the database with an “action” field that would contain the action to undo. Pretty complicated stuff for something as simple.

The javascript solution

One day while I was writing some javascript, the solution struck me : I could handle this completely with javascript. The basic idea is that whenever an action is completed, I can save its undo function in an array and call it whenever I need it.

Example, if I’m saving a user in a database with Ajax :

  1. I send the data to server to save a new user
  2. The server returns the ID of the new user
  3. I create a function with an ajax request that will send a delete of this user to the server
  4. I add the previous function to a stack of undo functions to be able to call it later

That, when I call that last function, it will undo that user creation without having to keep it in memory server-side.

Introducing jsKata.undo

I wrote a little something called jsKata.undo on GitHub that contains the logic describe earlier. It’s quite simple but it may grow over time. It has no requirement, not even jQuery. It’s very simple to use.

The complete doc is on GitHub.

1. Adding an action that can you can undo

I’ll use the “add a user” example with jQuery.

  1. $.post(
  2.   "http://yoursite.com/users/new", // The url to add a user
  3.   {name:"John Boucher"}, // The name of the user
  4.   function(newUser, textStatus, XMLHttpRequest) {
  5.     // This is called when the post is over
  6.     var newUserId = newUser.id;
  7.  
  8.     // We begin by creating a function to delete the user
  9.     var undoNewUser = function() {
  10.       $.post(
  11.         "http://yoursite.com/users/delete", // The url to delete a user
  12.         {id:newUserId} // The ID of the new user to delete
  13.       );
  14.     }
  15.  
  16.     // We add the undo function to the stack
  17.     jskataUndo.push(undoNewUser);
  18.   }
  19. );

2. Undo the last action you made

  1. jsKata.undo.undo();

3. Events

Each time there’s a change, an onChange event is called. To assign yours, simply write :

  1. jsKata.undo.onChange = function() {
  2.   // Show the undo button
  3.    $("#undoButton").show();
  4. }

There’s also an onEmpty event that is called when the stack of undoable actions is empty.

  1. jsKata.undo.onEmpty = function() {
  2.   // Hide the undo button
  3.    $("#undoButton").hide();
  4. }

Complete demo

There is a complete demo on GitHub as well as the HTML and Javascript code for it. Take a look!

  • Dan
    @Joao
    You're right, the undo history is lost if you close navigate out of the page. In this case, this is not a problem but more a feature. I didn't talk about I consider it strong enough for an undo system because you probably don't want to navigate out of the page and then come back and have an undo history that is still filled. It would be confusing. But I agree that in certain cases, you might want one. Then, there must be plugins that exist to do that kind of thing.

    @Ryan
    Thanks a lot, improvements are coming in a new version.
  • Ryan
    Good methodology for handling this feature addition. Thanks for sharing!
  • I think that it may have a problem...
    Because it's client side you've a problem, if the user click on delete p.e. and then close the site the information won't be deleted. Right?
  • Dan
    @Andrea
    I was already working with that idea and I talked about it on Reddit.

    @Charles
    Frank is right. You still have to write secure server code. And if someone is fool enough to do it, he will delete his own data.
  • Frank
    Charles,

    This undo code is client-side only. You still have to write some secure code on the server :)
  • Charles
    So what happens when client X looks at the code, and then decides to use some script to curl http://yoursite.com/users/delete?id=$n where $n goes from 0 to 10000?
  • What you are trying to reproduce is the Command pattern. But you obtained only half of it.
    Instead of saving the "undo" function in an array, encapsulate both the "do" and "undo" functions in an object and pass this object to a function that
    - call the "do"
    - save the object in an array
    When you want to undo pick the object from the array (but leave it there) and call the "undo"
    If you want to redo pick the object from the array and call the "do"
blog comments powered by Disqus