Friday, 8 June 2007

Sliding Door Tabs take 2

So, a while back I wrote a quick widget for making sliding door tabs. But it wasn't enough, I wanted *MORE*. I wanted them to be responsive to conditions.

For example, I have a resource User, and I want the "users" tab to stay open no matter which user page I'm on (index, show, edit etc)... *unless*, of course, it happens also to be the "my account" page (ie I'm viewing myself)... for which I already have a tab. This is a cut above just being "on" if we happen to perfectly match the current url.

So I rewrote the helper methods. Now my tabs come with optional separators (that can be independantly styled), and with the ability to pass in a conditional to override whether or not to show the tab as "on".

  def make_tab(t)
    # use the conditional if one was passed - default to current page 
    cond = t.has_key?(:cond) ? t[:cond] : current_page?(t[:options])
    content_tag "li", link_to(t[:name], url_for(t[:options])),
                             :class => (cond ? 'current' : nil)

  # Builds a tab that is just a separator
  def make_separator
    "<li class='separator'></li>"

  # Make tablist out of a given set of tabs.
  def build_tablist(tabs, do_sep = true)
    list = "<ul>"
    tabs.each_with_index do |t,i|
      list << make_tab(t)
      list << make_separator if do_sep && i != tabs.size - 1
    list << "</ul>"

And an example of use (that assumes existance of restful_auth)

<div id="topnav" class="tabNav">

    <% # standard set of tabs
      tabs = [] 
      if logged_in?
        tabs << {:name => 'dashboard', :options => dashboard_url}
        tabs << {:name => "my account", :options => user_path(current_user)}
        if current_user.is_admin?
          tabs << {:name => "widgets", :options => widgets_path, 
            :cond => (current_controller?(hash_for_widgets_path))}
          tabs << {:name => "users", :options => users_path, 
            :cond => (current_controller?(hash_for_users_path) && 
        tabs << {:name => 'logout', :options => logout_url}
        tabs << {:name => 'login', :options => login_url}
        tabs << {:name => 'forgot password', :options => forgot_password_url}
  <!-- tabbed browsing of main site areas --> 
  <%= build_tablist tabs, false -%>

Note that it also uses a convenience function: "current_controller?" This is below and could almost certainly be done better... but it currently serves to check if the given url-options match the current controller.

  # determines if the given set of options contains the current controller
  def current_controller?(options)
    return true if current_page?(options) # trivial case
    if options.respond_to?(:has_key?) && options.has_key?(:controller)
      return options[:controller] == controller.controller_name
    # try matching it - assumes it's a string url
    options =~ /#{controller.controller_name}/


Anonymous said...

Thanks for showing how to do tabbed navigation in Rails so efficiently.

I was using tabnav plugin but it is way too complex and I couldn't get it to work with ajax / link_to_remote. I was able to copy your code and modify it successfully to use link_to_remote. I now intend replacing all my tabnavs with variants of your helpers.

Taryn said...

Thanks - glad it's useful for you!

Taryn said...

Oh, and would you be willing to share your tips on converting for use with link_to_remote? :)

Anonymous said...

Update from "anonymous".

What I have is a set of navigation tabs and in each of these tabs I typically have a paginated table. Ideally I want both the tabs and the paginated output to be ajax enabled such that clicking on a tab just refreshes the tab content on the screen and clicking on the pagination controls just displays the next page inside the selected tab - a not unreasonable or uncommon pattern (imho).

I was using the tabnav plugin ( and will_paginate (

I came across several approaches to making will_paginate ajax enabled (e.g. I found the best solution to be at Redline (

But I could not get tabnav ajax enabled (probably due to my lack of understanding of the plugin). So I copied and modified your code samples as they were more comprehensible.

Code extracts are as follows:

*** rhtml ***
< div id="topnav" class="tabNav">
< % tabs = [
{ :name => 'Documents',
:options => {:url => {:action => "render_tabs", :tab => 'documents', :id => person},
:update => 'person_tab',
:before => "'spinner')",
:complete => "Element.hide('spinner')"}},
{ :name => 'Tasks',
:options => {:url => {:action => "render_tabs", :tab => 'tasks', :id => person},
:update => 'person_tab',
:before => "'spinner')",
:complete => "Element.hide('spinner')"}}
] %>

< %= build_tablist tabs, false % >
< /div>
< br />< br />
< div id="person_tab" >< /div>

*** tab helper ***

module TabsHelper
def make_tab(tab)
"< li>" + link_to_remote(tab[:name], tab[:options]) +"< /li>"
*** controller ***
def render_tabs
render :partial => "/documents/list", :locals => {:documents => @authored_documents}