UPDATE 1/15/08: dsHistory has improved greatly and has been moved to a new home. Read more here.
For the past few weeks now, I’ve been playing around with some different ways to implement support for the back button in many of my JavaScript-heavy web apps. In the past, nothing out there really seemed to suit my needs for one reason or another. Therefore, I made something to fill the gap. It’s not necessarily that there is a problem with what anyone else made; it’s just that nothing out there suited my needs. However, before I get into what I made, let’s give credit where credit is due and review what the great pioneers before me have made (in no particular order).
Really Simple History (RSH), Brad Neuberg
RSH was / is really great since it was the first good way to both keep track of history using JavaScript and allow developers to make their supercool apps keep URL state / allow for bookmarking. It was a bit difficult for me to easily jump into, but once I figured out how it worked internally, I found that RSH actually serializes any information you associate with a history item into JSON and puts it into the window hash. Nice, but I wanted more fine-grained control over the hash.
AJAX-Nav, Mike Stenhouse
The article by Mike in which he introduces AJAX-Nav is what really got me rolling on this project. He did an excellent job describing the stumbling blocks he ran into while building his solution and the different methods he created to get around them. Ultimately it seemed too tied into an actual AJAX request and was otherwise a bit difficult to get it to work the way I wanted it to work. As of this post, his demo is also broken. Still, an excellent solution.
dojo.io.bind, Alex Russell, et al (Google’s cached since original is missing?)
Now this is what I really wanted. With dojo’s bind, you bind the history to a function, not to hash args or anything else. You can also bind a hash along with it, but you have to keep track of the hash by yourself. Ultimately, as far as I can tell from looking at it though, everything is also bound to an AJAX server request. Awesome work / idea though.
YUI Browser History Manager, Julien Lecomte
This is probably the culmination of all previous attempts at solving the back / forward button issue. Without a doubt, and even though it is “experimental”, it is certainly the most robust history manager out there in terms of cross-browser support. Since it’s still pretty new, I highly recommend you take a good look at Lecomte’s YUI post to see how he built it and understand the choices JS developers have right now as well as how he overcame some of the development issues. I had dsHistory almost done at the time this came out, so I can’t say I really used it for much insight since it just wasn’t available. However, as far as the implemtation goes, it’s really incredible. In spite of this though, I still wanted to finish my little pet project since the YUI History Manager still didn’t do exactly what I was hoping to do.
So now I’m tossing my hat in the ring with my project, dsHistory. Check out my demo which explains how to get started, or keep reading for more info.
dsHistory internally works somewhat similar to the other history solutions that exist, but it is implemented more like dojo’s bind than anything else. It requires no supporting libraries, it checks in at just over 7kb when compressed, and it is easy to use. The history is thought of as a series of events that have functions attached to them, and the bookmarkable window hash data is designed to be controlled independently from the events (if it is even utilized at all, which I’ve found isn’t desired at times).
The project originally started as a quick solution for our dsSearchAgent real estate IDX / MLS / property search product, but there are very few, if any, shreds of that code that made it into this project. It has morphed into more than I thought it would be, but I’m pretty happy with it now that I’m finally done with my first release. My history manager’s name, dsHistory, is named as it is because we occasionally name our products with a ds prefix. I couldn’t think of a better name, so as a hat tip to my company, dsHistory it is.
Rather than give a very lengthy rundown as to how I developed dsHistory, I encourage you to first read through the methods everyone else has used up until this point since it has already been done before. With some exceptions, I took a very similar path but just changed the way it was ultimately implemented. Once you’ve reviewed the other projects, you may want to look through my commented source file to see more implementation details.
As I stated earlier, dsHistory is pretty easy to use and was designed so that functions would be called as a user moved forwards and backwards through the history. Specifically, the functions you add to the the history stack either with or without query vars should be thought of as the location / event / tab / whatever that the user is currently “in.” Therefore, the first function you add, no matter how you add it, won’t update the history event stack (see caveat below).
Since it is based on functions, I highly recommend you use a currying helper function such as Dustin Diaz’s curry() or, my personal favorite, Prototype’s bind(). Use whatever works best for you. Anyway, check out the example code below.
/* add a base function that will never be automatically removed from our history function stack. this is the way i intended dsHistory to be used. essentially, you should set this function to be the “base” state of your application, whether that is a function that displays a certain panel / tab combination, a certain piece of content, or whatever other function you want to use as your base state. */
dsHistory.addFunction(function() { alert(’base history’); });
// add two more random functions that will be called as a user goes back
dsHistory.addFunction(function() { alert(’history #2′); });
dsHistory.addFunction(function() { alert(’history #3′); });
/* now there are three events on the stack. going back once will call alert(’history #2′). going back again will call alert(’base history’). going forward once from this point will call alert(’history #2′). going forward once more will call alert(’history #3′). each function will have a single argument passed to it which will be either ‘back’ or ‘forward’, allowing you to take the appropriate action. */
/* we’ve now gone back twice and then forward twice. we’re at the end of the stack, so we’ll add some more fun to the history in the form of a bookmarkable query hash. */
dsHistory.setQueryVar(’my super cool key’, ‘key value’);
dsHistory.setQueryVar(’value without key’);
dsHistory.bindQueryVars(function() { alert(’query 1′); });
/* not only does the hash now contain the encoded keys / values (if a value exists), but dsHistory.QueryElements['my super cool key'] has a value of ‘key value’ and dsHistory.QueryElements['value without key'] has a value of ”. in fact, the dsHistory.QueryElements javascript object is loaded with the decoded keys / values when the page is loaded directly. this allows you to ultimately handle a bookmarked URL based on what comes in from the window hash */
// remove one of the query from the hash and then rebind the hash
dsHistory.removeQueryVars(’my super cool key’);
dsHistory.bindQueryVars(function() { alert(’query 2′); });
/* now there are five events on the stack. back once will call alert(’query 1′), back twice will call alert(’history #3′), and so on. since the first function is never popped off the stack, you can call dsHistory.setFirstEvent(functionArgument) to change it*/
dsHistory definitely has it’s caveats, and it’s not for everyone. However, it fits fine for me right now, and I suspect it may help others as well. In addition to the limitations pointed out by the YUI team / Julien, dsHistory has some additional known issues that you should consider before deciding to use it in your application.
- It doesn’t work with Opera and Safari. I don’t have a Mac, and the development resources required to get my solution working in Opera (my time) aren’t enough to justify the cost at this time.
- If a visitor is using Gecko and the first function added to the history stack is added with the bindQueryVars function (not recommended — see the first line in my usage block for a better way), a history item will immediately be created. The implication of this is that the user will be able to go back in the history and subsequently go forward without anything happening; this shouldn’t really be a big deal though. However, if a function is added to the history stack via dsHistory while the visitor is in this history “black hole,” window.history.forward() will be called so that the implementation remains consistent. It shouldn’t be a big deal, but it’s something you may want to be aware of.
- Don’t try to use “_serial” as a query var key as it is sometimes used in Gecko. Specifically, it’s used to keep track of whether or not the visitor is going forward or backward in certain circumstances.
- Finally, consider dsHistory an alpha / beta / experimental / test release. I’ve tested it to the fullest extend I know how on my machine, but given the nature of the project and my limited time, I’m certain there are bugs that I missed.
Now that I’m done with that, check out my demo to get started. It explains most everything you’ll need to know really. If you’d like to use it, you can download the zip. It contains the usage.html file, the uncompressed dshistory.js file, the compressed dshistory.compressed.js file, and the dshistory.html supporting asset file.
I welcome your comments, questions, bugs, and snide remarks.