Undo Made Easy with Ajax (Part 1.5)
This is the second half of the first part of the Undo Made Easy with Ajax series. If you are jumping in late, make sure to check out Part 1.
One of my readers, Alex Botero-Lowry, pointed out a big caveat to the entirely client-side event queue method of implementing Undo: If a user opens a second tab to a page they were viewing, those pages might be out of sync. In the to-do list example, if you delete three to-do items and then open a new tab/window to the same page, those to-do items would still be there. Why? Because the first page knows about the deletes (even though they are uncommitted) and the second does not.
Let’s take the client-side only approach and run with it. There is something nice about not having to mess with the back-end. We can fix the multiple-tab problem by syncing the event queue across all open pages with a cookie.
I’d like to stress that the event queue method is a light-weight solution to Undo. There are other, more robust ways of implementing Undo, like using a server-side based command pattern (a couple of my readers pointed this out, and I’ll probably cover it later in the series). But that’s beside the point. When we, as engineers, try to solve difficult problems, we often over-generalize and over-abstract our back-end solutions to cover all possible cases. When we do that, we miss the simple solutions that can help our users now. My point is that certain forms of Undo are low hanging fruit, so why not pick them? End of digression.
To reiterate, we are resolving what happens when the user deletes some items, opens a new tab to the same page, and those items do not appear to have been deleted. Let’s assume that the event queue holds the ids of the deleted objects, and that every time an item is deleted, or a deletion is undone, we write the queue to a cookie; when the page is closed, we clear the cookie. When the page is loaded, we check the cookie, and if it contains a non-empty list, we client-side “delete” the items and add them to the event queue. Now, any new tab/window opened to the page are guaranteed to be in sync with the old one.
The Tricky Bit
So, what happens when you make a change on one page and then switch back to the already open page? This could get complicated. We’re saved from having to deal with the full problem because the web is not a “push” technology. Currently, no web-app that I am aware of will, without needing to manually issue a refresh, update a page when it has been modified in a different tab. Users are used to refreshing a page in order to see changes. This means we need to correctly deal with the case that the user deletes or undoes deletes on the second tab, returns to the first tab, and hits refresh.
To solve this problem, we need to make the assumption that the last tab/window that the user was editing contains the most up-to-date content state. It’s a reasonable assumption because the last thing the user worked on is by far the most likely to be what the user thinks of as the latest version.
When the user refreshes a page, the onUnload callback gets called. In this callback we need to check whether this page’s event queue is the same as the last saved-in-a-cookie event queue (remember that the event queue gets saved to a cookie when a user deletes or undoes a delete). If two two event queues are the same, the page is up-to-date and we go ahead with committing the changes to the server. If the two events queues are not the same, then the current page needs to be synced with the saved-in-a-cookie event queue, thus bringing it up-to-date.
Play around with it, then open a new browser window. Now, change something in the new window and then refresh the old window. It all updates correctly. Good times.
A side benefit of the cookie route is that it ameliorates the already rare case that the user loses work due to a browser crash. When the browser crashes, all changes are saved in the cookie, so when users load up the page again, their changes will still be in effect.
The event queue system with a cookie synchronization system is a low-hanging Undo fruit. It won’t work in every situation, but it will work for a surprising number of cases. It’s well worth the relatively small implementation time.
Next week, I’ll write about solving Undo for time-sensitive actions.