Free-for-all: Tab Helper
Koz and I have opinions, and we like sharing them. This site was designed as a platform for us to express those opinions.
You all have opinions, too, and some of you even have opinions that (gasp!) differ from ours. Don’t try to deny it—we read the comments on this site, too.
Well here’s your chance play the game. Nate Morse sent us an email shortly after RailsConf in which he described a problem that Koz and I would like to present to you, our readers. How would you respond?
I’ve been trying to figure out a clean way to implement a tabbed view on a website where the selected tab is written out in a <span /> element and anything unselected is a link to the particular action. So far, this is what I’ve come up with.
1 2 3 4 5 6 7 8 9 def tab_to(name, options = {}) url = options.is_a?(String) ? options : url_for(options.merge({:only_path => false})) current_url = url_for(:action => @current_action, :only_path => false) if (url == current_url) content_tag(:span, name) else link_to(name, options) end endThe @current_action variable is set in a before filter and used to determine if the tab I’m specifying is for the current action. Some of this code is lifted straight from the source of link_to (which is why it will accept either an options hash or a url) and is probably overkill. Also, it seems kind of hokey that I’m setting the :only_path key in a couple of places just to get the urls in a standard form. Is there a better way to do this?
Please post your responses in the comments. Because the comments aren’t editable, you should probably draft your response externally and then paste it in. You can use textile codes to format the text. For syntax-highlighted code snippets, just enclose the snippets in <macro:code> tags, with the lang attribute set to the language of the snippet (e.g., “ruby”, or “rhtml”). For example:
At the end of the week, Koz and I will write up a summary, highlighting a few of the responses. Read, set, go!


blehg, feel free to delete that. Let’s hope I don’t frig it up again.
This will put the links inside of a as well as the text for the current page, but that seems reasonable.
Maybe this would work:
This isn’t exactly a code solution per se, but there is a tabnav plugin at http://blog.seesaw.it/pages/tabnav that deals with this quite nicely.
Would Bruce Williams’ folder_for play well here?
<%= tabs_for("cat", "dog", "index") %>... or if the link names need to differ from the action names, pass in a hash. The actual implementation code is pretty much like the last examples, albeit with a few minor changes. It’s nice when the helper call is small and easy to read in the view :) Whatcha’ think ?
+1 for link_to_unless_current with a block for the alternate markup, as James suggests. We had a discussion on the SDRuby mailing list about this issue back in February and my favorite of the ideas was using link_to_unless_current and a block.
It’s worth noting that the exact condition for primary navigation tabs is going to vary a lot from site to site, which is where the more generic link_to_unless would come in. Here’s a contrived example that disables the link if the current view is merely in the same controller:
A slightly different, more flexible, approach would be to pass the condition in the params and use a default value of nil to assume it’s the current page. I use link_to_if here because it seems to make more sense in actual usage.
I take a completely different approach. I ID the body of the page with the name of the current controller. Then I use a descendent CSS selector to highlight the current tab based on the body id and an id given to each link. I don’t bother with replacing the current tab link with a span. If the user wants to click that link again… then it’s the same as refreshing. Totally up to them.
With html like
I would use CSS like this
#users #usersNav, #comments #commentsNav, #posts #postsNav { background:red; font-weight:bold; }One aspect you guys may want to consider is, is it always a good idea to couple your navigation tightly to your controllers? What could you do to avoid this?
@Koz, well the answer to that is to use named routes. They provide that extra level of indirection that decouples the navigation from a specific controller since you could remap the route if you needed to.
An interesting idea would be to leverage the routing recognition code to set “current” tab.
I had implemented something similar. here is my suggestion:
Here is an example about how to use it:
The result:
I also use ‘in_action?’ and ‘in_controller?’ helpers for conditions in some views.
I forgot to edit the tab_link_to to use span in stead of link with selected class :)
Here is the update:
Well, since there’s already a fine helper to determine if you’re about to contruct a reference to the current page, use that. Simple decoration of the span tag or the link is through normal html_options like with many other helpers and in case the active tab needs something extra special, a block given to the tab_to() will be allowed to take precedence over the simple span tag.
As a followup from my earlier post, the tab API might be better expressed more like so:
OT… this is a great discussion ! It’s like a Rails version of: http://www.rubyquiz.com/
Excuse for my english,but it s very difficult to explain my idea in your language. I have a original approach for this problem. I dont like tab_for because its couple design and behavior.
so i prefer my approach because its more flexibleactivetab take a erb block,search the DOM element of current tab, and replace the link by a span.
examples of utilization
For those interested, I’m Nate (the original submitter) and in the days since asking Jamis and Koz this question (but prior to the creation of this thread) here is what I’ve decided to do…
I’ve scrapped the tab_to helper completely.
Originally, it’s purpose was to vary the html output of the section links so that I could style the selected tab differently from those that were unselected. This was fine for basic navigation, but once I started implementing the website it became pretty clear that one tab per action just wasn’t going to cut it. Not only that, but span tags weren’t really necessary just to style a selected tab.
So, I borrowed a technique from Douglas Bowman over at A List Apart (http://alistapart.com/articles/slidingdoors2/) and used his “Sliding Doors” method to implement my CSS tabs. The following is my navigation partial:
Now, instead of styling my tabs based on the current action (or even the current controller), I write out an identifier in the body element of the page that indicates which tab is selected. This has the advantage that if I choose to select none of the tabs, I can easily do that as well.
For example, the following is a snippet from my stylesheet:
#dashboard #tab-dashboard, #time_entry #tab-time_entry, #reports #tab-reports, #setup #tab-setup { background-position: 0 -150px; border-width: 0; } #dashboard #tab-dashboard a, #time_entry #tab-time_entry a, #reports #tab-reports a, #setup #tab-setup a { background-position: 100% -150px; padding-bottom: 10px; color: #fff; }This still isn’t quite bulletproof as I have to modify the CSS anytime a new tab is added. However, new tabs are going to be a pretty rare event, and that’s something I’m willing to live with.
Thanks to all who have commented so far. I’ve found all of the answers really fascinating. I particularly liked how clean James Whiteman’s solution was for the original problem. You’ve all opened my eyes to some great examples that should’ve been staring me in the face all along.
If anyone would like to comment on my current solution, I’d be very interested to hear those as well. I’m really liking this new format. Nice work everyone!
And 100 bonus points go to Mr eel for coming up with the same approach.
@Nate – Have you considered using a css class for your “active” tab? That way you wouldn’t have to add to your stylesheet for new tabs.
@TJ – Good point. The syntax John came up with earlier might be well-suited for this kind of thing. For example:
Where tab_to would write out the selected tab as a member of the “active” class. Then, the navigation partial might look something like this.
And the CSS could be simplified to the following…
#tabs .active { /* custom style goes here */ }Cool!
+1 for railsquiz!
What about this AJAX approach?
http://actsasflinn.com/Ajax_Tabs/index.html
I’ll admit that I haven’t quite grasped it all yet, but I’d kept it on file for when I start implementing tabbed content.
This has been a lot of fun, and I’ve already learned quite a bit from reading the other responses so far – even without the summary. I think this really would be an excellent format for some sort of informal railsquiz.
The problem I had with one of the sites I developed was that the navigation had to have multiple levels. This lead to a data driven approach which works very well.
(Note: this is some of the first Ruby code I ever wrote. I know there are better ways to code it; the thing that is important is that the model data drives the navigation rendering.)
First, there is a model for the navigation elements:
Then the navigation data structure is established in the controller:
Finally, the view renders a partial with the data:
With the partial template:
The advantage of this model driven approach is that it’s very flexible (we used it throughout a very large site.)
Someday, I’d like to implement a DSL for the model to make it even easier to use.
-ch
Jamis, Koz: thanks for this blog – what a wealth of information!
I didn’t bother with the active tab being a span for my site. I’ve got the following in my application helper:
I can’t remember where I pilfered that from. Oh, and parameters_for_method_reference is deprecated as of 4 weeks ago (moved over the “the edge” last night!)
@Koz: Absolutely right—use named paths or something other than an assumed relationship to the controller.
@Nate Morse: The sliding windows approach works very well. Not only does it give you a very flexible solution (using classes rather than ID’s you can port it to other areas/projects) but the html is quite expressive of what you’re rendering: a list of navigational options for the user. The big advantage is that if someone has css turned off, the rendered list still makes sense.
Combine that with what @jamie Lawrence posted and it’s even more powerful. Since you’re using css based on the
@Jamie Lawrence: The one change that I’d make today (I’ve implemented something very similar) is that I’d make the panes from dl/dd/dt’s in the markup. It’s similarly expressive of the intent: you’re defining a data set related to the nav element. I have not thought about this long enough but it may very well be possible to skin w/css such that the definition list (dl) replaces the ul from the sliding window technique, the defined term (dt) takes the place of the li, and the description (dd) contains the pane. I guess I’ve got something to do tonight…
Update to my previous post. I worked out the html/css necssary to use a dl/dt/dd structure for holding together a set of tabs. It’s very similar to to the ‘sliding windows’ solution at “A List Apart” but I think uses the markup in a better manner to express the relationship between the tab (dt) and the pane (dd). You can check it out here.
For my main navigation tabs I started out with something very messy.
I set a variable in my controllers with the tab name and used that in my tabs partial to add the ‘selected’ class to the corresponding tab. But that felt more like M(VC) than MVC.
So I decided on mapping between controller_name and the tab names inside the tabs partial. At least the controller code didn’t have any mention of tabs! But I still felt bad. It felt like I had joined my controllers and views together more tightly and enforced some 1-1 controller-tab thing.
So here is where I am now.
In my views I say
Yes, It ain’t the DRYest. In my edit/new/show views for ponies, I also have to say tab :ponies. But I can live with that. My views are not tied to any controller, and I’ve been sleeping better.
ApplicationHelper defines tab as simply:
And my tabbed navigation partial defines the tabs and sets the ‘selected’ class on the @selected_tab:
I’m not totally happy still, but it feels like the right direction.
I’ve read every post on your blog, and can’t wait to see the railsway :)
erm.. can you change “My views are not tied to any controller” to “My tabs are not tied to any controller”
An edit button would be nice ;)
+1 for Mr Eel’s CSS approach. It’s the approach I’ve been using for a while and it’s easy to maintain, very straightforward, and I it keeps the code lean.
Jamis/Koz, Without meaning to be pushy but have you had the time to write up a summary of these responses? They all look very interesting but as a completed novice, I’d love to hear some experienced opinions!
Better late than never. Here’s what I use:
This way you can set the @current_section variable to anything that makes sense, and the selected tab is not necessarily tied to the controller.
@Jamie, we’ve got a summary post in the works. I do apologize that it has taken SO long for us to wrap this up, but it’s coming, hopefully tonight. It’s hardly a complicated wrap-up, it’s just a bunch of other stuff hit us both at about the same time. We’re working to get back on track, though.
Here’s a pseudocode representation of how i do this. The highlighting behaviour isn’t automatically coupled with the current controller so there’s a little more work to do, but with the advantage that it’s a more flexible system that allows you to have multiple ‘tabs’ highlighting for a particular page (useful for when you have a navigation hierarchy that uses sub-sections).
The crux of it is that you need to populate an @ancestor_urls in your controller for each ‘page’ of the site, and then use a wrapper helper to create the links that might need highlighting. If the link being rendered by the helper happens to point to a url that’s in the @ancestor_urls array, the ‘highlighted’ class will be applied to that link.
# the helper def context_sensitive_link_to text, options={} # check if the url the link points to is one of the ancestor urls for the current page # just use the controller and action to compare (not id, or any extra params) # If this link is pointing to one of the ancestor id's add the 'ancestor_of_current_page' class to the link end # in the controller @ancestor_urls=[ {:controller=>'general'}, {:controller=>'works'} ] # in your view <%= context_sensitive_link_to "my link text", { :ancestor_urls=>@ancestor_urls, :url=>{:controller=>:my_controller} } %>Being a novice with Ruby and Rails, I have NO IDEA what the output of this ‘tabbed helper’ is supposed to be ! ;)
Obviously everyone else here knows what it is :)
I pasted the “html / css” solution here :
http://mstramba.com/tabbed.html
... as all the rest of the posted Rails code were “fragments”, which I had no idea where they were supposed to go into a Rails application.
It just looks like a smple list !
What is it supposed to do ?
Mike