Charly's Tech Blog

home

Tabbed Navigation for Rails

04 Aug 2009

When I start a new rails app and write a tabbed menu, I always postpone the moment I'll determine how it's going to set the current tab. And when it comes to it, I usually do quick and dirty stuff with the controller name, the @category.name or whatever. But last time, facing a step by step form you could navigate through with tabs, I thought "enough of it I need a solution once and for all".

Sooo, looking at plugins first tabnav seemed to be the reference. But I wanted something i could easily tweek and understand, and this was just overkill for me needs.

Then I looked at the free for all tab helper which has some very interesting solutions, the most elegant being the one relying only on css and the body tag (no logic involved). But that wasn't flexible enough, they were all making the assumption the tabs would switch controllers.

So having to get my hands dirty I first refreshed my memory with my favourite source of hints and came up with what I believe is a very elegant and flexible solution.

#contenth
  %ol#steps_menu
    - tabs_for :step => active_step do |t|
      = t.link_to "1. start", :step=> "1start"
      = t.link_to "2. price", :step=> "2price"
      = t.link_to "3. bill", :step=> "3bill"
      = t.link_to "4. payment", :step=> "4payment"
      = t.link_to "5. end", :step=> "5end"

  =render :partial => "/admin/orders/steps/step_#{active_step}"

And the Helper :

module TabsHelper

  # takes the block
  def tabs_for(current_tab, &block)
    yield Tab.new(current_tab, self)
  end

  class Tab
    def initialize(current_tab, template)
      @current_tab = current_tab
      @template = template
    end

    def link_to(*args)
      "<li #{active_class(*args)}>#{@template.link_to(*args)}</li>"
    end

  private
    def active_class(*args)
      if @current_tab.all? { |k, v| args[1][k] == v  }
        "class = 'active'"
      end
    end
  end #class Tab

end

This is minimalistic but you can see that it is very easily extendable. The main idea is that you are sendind to the tab class a matcher that is going to set the current tab.

My matcher was a bit complex as you can see below(it also sets the partial to render), but keep in mind that you could do exactly the same like this :

tab_for :controller => controller_name do |t|
...

and it works with no further logic.

module OrdersHelper

  # helper to determine the active order step given :
  # 1. a params[:step]
  # 2. if none, according to the order's state
  def active_step
    @active_step ||= if params[:step]
        params[:step]
      elsif %(opened).include? @order.state
        "1start"
      elsif %(submitted).include? @order.state
        "2price"
      elsif %(evaluated).include? @order.state
        "3bill"
      elsif @order.state=="billed"
        "4payment"
      else
        "5end"
      end
  end
end

What I appreciate most is that the logic for matching the tab is kept seperate ( it only relies on the params inside the links ) and the "tab_for do" syntax is very railsish & familiar.