Inception:
A few years ago I built a web app that had an unusual, if not suspect, UX feature. It seemed like the right way to go at the time, and I think it actually turned out well. Its been in production for a few years and has only had a few usability issues which I’ll discuss below.
Requirements:
The user should be able to search on things, drill down, perform
various CRUD actions, and then click a series of back-links, or a
breadcrumb trail, to return to the search results. Actually, that’s a simplification of the possible users interactions – it could get very elaborate.
Problem already solved?
This might seem like a simple problem to solve. Just somehow remember where the user just was, hardcode the back-links, or use something like javascript history.go(). But those wouldn’t work because the user might have to go back through various CRUD operations, success and failure screens, and probably would eventually end up in an embarrassing back-link loop.
But OMG is that RESTful?!?
Some data is stored in the session – but does that mean its not RESTful? I’m not sure and I’m also not that concerned. REST advocates are like libertarians; they have good ideas, but they usually take those ideas too far, and probably don’t practice what they preach anyway.
How about sessions that store authentication information? Are they RESTful? If that’s all they are doing, then yes they are. However, if along with that authentication information, additional information is used to differentiate, by user, what looks like the same resource , then I think the RESTful purity is out the window – and this is a practice for almost all web apps, including those built by REST evangelists.
But is a back-link or breadcrumb trail even a part of the resource at all, or just considered part of the interface?
So then how?
So how to make a back-link do the right thing? Is there a pattern it could follow to figure out what “back” always correctly meant? Turns out it depends on the structure of your web app.
Wrong structure for backstack
If your web app’s structure looks like the image below (what nerds would call a complete graph), with squares being web pages and lines being links, then I don’t think there would be much hope because the user would eventually find themselves in a back-link loop, and the concept of back wouldn’t mean that much anyway.
Right structure for backstack
If your web app’s graph follows the characteristics listed below (or a targeted subset of your web app), and I believe most do, then the backstack’s algorithm works well. I don’t have a formal proof of this, which is preferable since I’d just screw it up, or get bored and never finish.
- Its graph is “layered”.
- Links don’t jump layers, they can only travel one layer at a time
- Free travel is allowed within a layer, so a layer itself could be considered a complete graph.
Below is an idealized web app’s graph exemplifying of the rules listed above.
You can think of page A as my old web app’s search results page, B and C are the landing pages for those “things” that I searched on, and D, E, and F are various screens for B and C.
If the user drills down from A, through B to D, backstack
has to build links that know to go back to B and not to C, and then back to the results page of A.
The way backstack works is it builds up a “stack” (you didn’t see that one coming?), using what it knows about your web apps “graph”, and uses that stack to figure out the way back.
Describing the graph to backstack
Before we get to the stack building algorithm, how does backstack know the graph of your web app?
In my old site, where backstack was born, the graph description is an ugly, hard to follow mess in application_controller.rb. But because I made this into a rails plugin I had higher standards to meet.
You simply tell backstack which actions potentially back-link to which other actions. In the below image, the red arrows is all backstack needs to know.
Lets say that each row of the graph is handled by a single controller. A is the only action in the c1 controller, B and C are in the c2 controller, and D, E, F are in the c3 controller.
Tell backstack that B and C back-link to A. Because A is in in another controller, we need to be more specific using ‘railsy’ looking # notation
class C2Controller "c1#a" backstack :c => "c1#a" def b; end def c; end end
You could call backstack multiple times, but there’s an array shorthand that lets you group actions together. The follow backstack call says “actions d, e and f in the current controller go back to b and c in controller c2.”
class C3Controller ["c2#b", "c2#c"] def d; end def e; end def f; end end
Those three calls to backstack are all we need to describe the above graph of the website to backstack.
The stack and its algorithm
Using this graph information backstack dynamically builds a stack to keep track of ‘back’. It uses these following rules to do it.
Stack elements are the array [action, fullpath, label], fullpath is the actual URL, a.k.a. request.fullpath, label is for breadcrumb text.
- Start rule – If stack is empty push current page on stack
- Stacking rule – If current page back-links to what’s already at top of stack then push current page on stack, building up stack.
- Rewind rule – If current page is already on stack, rewind past it, and push current (rewind and push because fullpath might have changed). This could possibly shrink stack (certainly if user clicked on a breadcrumb link)
- Sidestep rule – If current page doesn’t close to top of stack, replace top of stack with current page. (like B to C and D to E to F)
The simple back link
<%= backstack_link "Back" %>
The breadcrumb trail
If you want to use backstack to create breadcrumb trails you need to pass it crumb labels. Just wrap the actions, along with their labels, in a hash. Only label the actions in the current controller, not the actions they back-link to. Also keep in mind that ruby can get a little confused about method parameters when they get complicated, so make sure you enclose them in parentheses.
class C1Controller "Alpha"} => nil) def a; end end class C2Controller "Bravo"}, {:c => "Charlie"}] => "c1#a" def b; end def c; end end class C3Controller "Delta"}, {:e => "Echo"}, {:f => "Foxtrot"}] => ["c2#b", "c2#c"]) def d; end def e; end def f; end end
Backstack supports breadcrumb trails with the method backstack_trail().
It becomes an iterator if passed a block:
<% backstack_trail do |c| %> <% if c[:fullpath] == request.fullpath %> <%= c[:label] %>/ <% else %> <%# make clicky %> <%= link_to(c[:label], c[:fullpath]) %>/ <% end %> <% end %>
A call to backstack_trail() without a block returns an array to make it easier to join your breadcrumbs. (Don’t have to deal with anoying newlines and a trailing separator like the above example.)
<% crumbs = backstack_trail.map do |c| if c[:fullpath] == request.fullpath c[:label] else link_to(c[:label], c[:fullpath]) end end %> <%= crumbs.join("/").html_safe %>
Usability Issues
Having a back-link that just does the right thing has only confused a few users as far as I could tell from feedback. I suspect those users were more familiar with how websites normally behave and were a little thrown off. Others were probably confused that there was a difference between a back link and the browsers back button. I think both of these difficulties can be remedied by using a breadcrumb in place of, or along with, the back-links.
Diagnostics
If things get hinky you can dump diagnostic info to your log file with backstack_dump()
class ApplicationController < ActionController::Base after_filter :backstack_dump end
Get and use backstack
Find the source here:
https://github.com/kswope/backstack
Find the gem here:
https://rubygems.org/gems/backstack
Install the gem like so:
gem install backstack
Unsurprisingly you need this line in your Gemfile:
gem 'backstack'