Free-for-all: Tab Helper

Posted by Koz Monday, June 04, 2007 00:07:00 GMT

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
end

The @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:

<macro:code lang="ruby"> def foo(a,b) return a + b end </macro:code>

At the end of the week, Koz and I will write up a summary, highlighting a few of the responses. Read, set, go!

Comments

Leave a response

  1. KjellJune 04, 2007 @ 01:10 AM

    blehg, feel free to delete that. Let’s hope I don’t frig it up again.

    1
    2
    3
    4
    5
    6
    
    def tab_for(*args)
      content_tag(:span, link_to_unless_current(*args), :class => "tab #{args.first}")
    end
    # tab_for('new fiddle', new_fiddle_path)
    # tab_for('all fiddles', fiddles_path)
    # tab_for('all fiddles', :action => :index) even works all right

    This will put the links inside of a as well as the text for the current page, but that seems reasonable.

  2. James WhitemanJune 04, 2007 @ 01:27 AM

    Maybe this would work:

    1
    2
    3
    4
    5
    
      def tab_for(name, options = {})
        link_to_unless_current(name, options) do
          content_tag(:span, name)
        end
      end
  3. ShadowfiendJune 04, 2007 @ 02:00 AM

    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.

  4. Adam KeysJune 04, 2007 @ 02:14 AM

    Would Bruce Williams’ folder_for play well here?

  5. DylanJune 04, 2007 @ 02:20 AM
    From a view perspective, it would be nice to do something like:
    
    
    <%= 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 ?

  6. Nick ZadroznyJune 04, 2007 @ 03:54 AM

    +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:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    # tab_for('Kitties', hash_for_kitties_path)
    # tab_for('Puppies', hash_for_puppies_path)
    
    def tab_for(name, options={})
      condition = (options[:controller] == controller.controller_name)
      link_to_unless(condition, name, options) do
        content_tag(:span, name)
      end
    end
  7. Nick ZadroznyJune 04, 2007 @ 04:19 AM

    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.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    
    # tab_for "Home", home_path # => uses current_page
    # tab_for "Kitties", kitties_path, controller.controller_name != 'kitties'
    # tab_for "Puppies", puppies_path, false # => always disabled
    
    def tab_for(name, options, condition=nil)
      condition = current_page?(options) if condition.nil?
      link_to_if(condition, name, options) do
        content_tag(:span, name)
      end
    end
  8. Mr eelJune 04, 2007 @ 05:07 AM

    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

    1
    2
    3
    4
    5
    6
    
    <body id="users">
      <ul>
        <li><a href="/users" id="usersNav">Users</a></li>
        <li><a href="/comments" id="commentsNav">Comments</a></li>
        <li><a href="/posts" id="postsNav">Posts</a></li>
      </ul>

    I would use CSS like this

    1
    2
    3
    4
    5
    6
    
    #users #usersNav,
    #comments #commentsNav,
    #posts #postsNav {
      background:red;
      font-weight:bold;
    }
  9. KozJune 04, 2007 @ 05:10 AM

    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?

  10. Christian RomneyJune 04, 2007 @ 12:20 PM

    @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.

  11. Ernesto JiménezJune 04, 2007 @ 01:26 PM

    I had implemented something similar. here is my suggestion:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    
      def check_in(element, list)
        raise ArgumentError, "#{list.class} is not a Hash" unless list.class == Hash
        list[:only]   = [list[:only]].flatten if list[:only]
        list[:except] = [list[:except]].flatten if list[:except]
        return (    list[:only].include?(element))   if list[:only]
        return (not list[:except].include?(element)) if list[:except]
        raise ArgumentError, 'You need to specify :only or :except'
      end
    
      def in_action?(actions={})
        check_in(params[:action],actions)
      end
    
      def in_controller?(controllers={})
        check_in(params[:controller],controllers)
      end
    
      def tab_link_to(name, options = {}, html_options = nil, *parameters_for_method_reference)
        options[:controller] ||= params[:controller]
        options[:controller]   = options[:controller].to_s
        options[:action] = options[:action].to_s
        if options[:action]
          html_options = {} unless html_options
          if in_action?(:only => options[:action]) and in_controller?(:only => options[:controller])
            html_options.merge!({:class => "selected"})
          end
        end
        return(link_to name, options, html_options, *parameters_for_method_reference)
      end

    Here is an example about how to use it:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    
      <!-- Actual tab -->
      <%= tab_link_to 'Index', :action => :index %>
      <%= tab_link_to 'Index', :action => 'index' %>
      <%= tab_link_to 'Index', hash_for_posts_path %>
    
      <!-- Exaple of inactive tab with a path link -->
      <%= tab_link_to 'Edit', :action => :edit, :id => 1 %>
      <%= tab_link_to 'Edit', :action => 'edit', :id => 1 %>
      <%= tab_link_to 'Edit', hash_for_edit_post_path(:id => 1) %>
    
      <!-- Exaple of inactive tab with a url link -->
      <%= tab_link_to 'Edit', :action => :edit, :id => 1, :only_path => false %>
      <%= tab_link_to 'Edit', :action => 'edit', :id => 1, :only_path => false  %>
      <%= tab_link_to 'Edit', hash_for_edit_post_url(:id => 1) %>

    The result:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    
      <!-- Actual tab -->
      <span class="selected">Index</span>
      <span class="selected">Index</span>
      <span class="selected">Index</span>
    
      <!-- Exaple of inactive tab with a path link -->
      <a href="/posts/1;edit">Edit</a>
      <a href="/posts/1;edit">Edit</a>
      <a href="/posts/1;edit">Edit</a>
    
      <!-- Exaple of inactive tab with a url link -->
      <a href="http://0.0.0.0:3000/posts/1;edit">Edit</a>
      <a href="http://0.0.0.0:3000/posts/1;edit">Edit</a>
      <a href="http://0.0.0.0:3000/posts/1;edit">Edit</a>

    I also use ‘in_action?’ and ‘in_controller?’ helpers for conditions in some views.

  12. Ernesto JiménezJune 04, 2007 @ 01:29 PM

    I forgot to edit the tab_link_to to use span in stead of link with selected class :)

    Here is the update:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    
      def tab_link_to(name, options = {}, html_options = nil, *parameters_for_method_reference)
        options[:controller] ||= params[:controller]
        options[:controller]   = options[:controller].to_s
        options[:action] = options[:action].to_s
        if options[:action]
          html_options = {} unless html_options
          if in_action?(:only => options[:action]) and in_controller?(:only => options[:controller])
            return content_tag(:span, name, :class => 'selected')
          end
        end
        return(link_to name, options, html_options, *parameters_for_method_reference)
      end
  13. Rob BiedenharnJune 04, 2007 @ 08:58 PM

    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.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    
    def tab_to(name, options = {}, html_options = {}, *parameters_for_method_reference, &block)
      if current_page?(options)
        if block.nil?
          content_tag(:span, name, html_options)
        else
          block.call(name)
        end
      else
        link_to(name, options, html_options, *parameters_for_method_reference)
      end
    end
  14. JohnJune 04, 2007 @ 09:34 PM
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    
    def self.tab(name, options = {})
      before_filter(options) do |controller|
        controller.instance_variable_set('@current_tab', name)
      end
    end
    
    class FooController
      tab :foo
      tab :bar, :only => :index
    end
    
    def tab_to(name, tab, options = {})
      link_to_if(tab.to_s == @current_tab.to_s, name, options) do
        content_tag(:span, name)
      end
    end
    
    # tab_to('Foo', :foo, foos_path)
    # tab_to('New Bar', :bar, new_bar_path)
  15. JohnJune 04, 2007 @ 10:01 PM

    As a followup from my earlier post, the tab API might be better expressed more like so:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    
    class Tab
      def initialize(context)
        @context = context
      end
    
      def current_tag
        @context.instance_variable_get('@current_tab')
      end
    
      def method_missing(tab, name, options = {})
        @context.link_to_if(tab.to_s == current_tab.to_s, name, options) do
          @context.content_tag(:span, name)
        end
      end
    end
    
    def tabs(&block)
      yield Tab.new(self)
    end
    
    # <% tabs do |tab| %>
    #   <%= tab.foo 'Foo', foos_path %>
    #   <%= tab.bar 'Bar', bars_path %>
    # <% end %>
  16. DylanJune 04, 2007 @ 10:28 PM

    OT… this is a great discussion ! It’s like a Rails version of: http://www.rubyquiz.com/

  17. thegnouJune 05, 2007 @ 12:54 AM

    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 flexible
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    
    require 'hpricot'
      def activetab(options={:search=>".active"},&block)
        tabs=Hpricot(capture(&block))
        activetab=if options.has_key?(:text)
                            tabs.at("a[text()*=#{options[:text]}]")
                      else
                            tabs.at(options[:search]).at("a")
                      end
        activetab.swap(content_tag(:span,activetab.inner_text)) 
        concat(tabs.to_html,block.binding)
      end

    activetab take a erb block,search the DOM element of current tab, and replace the link by a span.

    examples of utilization

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    
    <% activetab  :text=>"Items" do%>
            <ul id="menu">
                    <li>
                    <a  href="/documents/">
                    My documents
                    </a>
                    </li>
                    <li>
                    <a  href="/documents/edit">
                    Edit
                    </a>
                    </li>
                    <li>
                    <a  href="/items/">
                     Items
                    </a>
                    </li>
             </ul>
    <%end%>
    <% activetab  :search=>"#items" do%>
            <ul id="menu">
                    <li id='documents'>
                    <a  href="/documents/">
                    my documents
                    </a>
                    </li>
                    <li id='documents_edit'>
                    <a  href="/documents/edit">
                    edit
                    </a>
                    </li>
                    <li id='items'>
                    <a  href="/items/">
                     Items
                    </a>
                    </li>
             </ul>
    <%end%>
    <% activetab   do%>
            <ul id="menu">
                    <li>
                    <a  href="/documents/">
                    my documents
                    </a>
                    </li>
                    <li>
                    <a  href="/documents/edit">
                    edit
                    </a>
                    </li>
                    <li class='active'>
                    <a  href="/items/">
                     Items
                    </a>
                    </li>
             </ul>
    <%end%>
  18. Nate MorseJune 05, 2007 @ 02:44 AM

    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:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    
    <div id="tabs">
     <ul class="lhs-tabs">
       <li id="tab-dashboard"><%= link_to('Dashboard', home_url) %></li>
       <li id="tab-time_entry"><%= link_to('Time Entry', time_entry_url) %></li>
       <li id="tab-reports"><%= link_to('Reports', reports_url) %></li>
     </ul>
     <ul class="rhs-tabs">
       <li id="tab-setup"><%= link_to('Setup', setup_url) %></li>
     </ul>
    </div> 

    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:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    
    #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!

  19. Nate MorseJune 05, 2007 @ 03:02 AM

    And 100 bonus points go to Mr eel for coming up with the same approach.

  20. TJ StankusJune 05, 2007 @ 03:32 AM

    @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.

  21. Nate MorseJune 05, 2007 @ 04:25 AM

    @TJ – Good point. The syntax John came up with earlier might be well-suited for this kind of thing. For example:

    1
    2
    3
    
    class DashboardController
      active_tab :dashboard, :except => ['account', 'help']
    end

    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.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    
    <div id="tabs">
      <ul class="lhs-tabs">
        <li><%= tab_to('Dashboard', :dashboard, home_url) %></li>
        <li><%= tab_to('Time Entry', :time_entry, time_entry_url) %></li>
        <li><%= tab_to('Reports', :reports, reports_url) %></li>
      </ul>
      <ul class="rhs-tabs">
        <li><%= tab_to('Setup', :setup, setup_url) %></li>
      </ul>
    </div> 

    And the CSS could be simplified to the following…

    1
    2
    3
    
    #tabs .active {
      /* custom style goes here */
    }

    Cool!

  22. Jesse AndrewsJune 05, 2007 @ 04:25 AM

    +1 for railsquiz!

  23. Jamie LawrenceJune 05, 2007 @ 12:46 PM

    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.

  24. James WhitemanJune 05, 2007 @ 08:19 PM

    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.

  25. Craig HockenberryJune 07, 2007 @ 02:04 AM

    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:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    
    class NavigationElement
    
      attr_accessor :name, :tooltip, :action_name, :children, :parent
      
      def initialize(name, tooltip, action_name)
        @name = name
        @tooltip = tooltip
        @action_name = action_name
        @children = []
        @parent = nil
      end
    
      def add_child(element)
        element.parent = self
        @children += [element]
      end
      
      def has_children?
        @children.length > 0
      end
    
      def has_action_named?(action_name)
        @action_name == action_name
      end
      
      def has_child_action_named?(action_name)
        result = false
        for child in @children
          if (child.action_name == action_name)
            result = true
            break
          end
        end
        result
      end
    end

    Then the navigation data structure is established in the controller:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    
      @@navigation = []
      @@navigation += [NavigationElement.new("One", "tooltip 1", "one")]
      @@navigation += [NavigationElement.new("Two", "tooltip2", "two")]
      parent = NavigationElement.new("Three", "tooltip3", "three")
      child = NavigationElement.new("Three A", "tooltip3a", "three_a")
      parent.add_child(child)
      child = NavigationElement.new("Three B", "tooltip3b", "three_b")
      parent.add_child(child)
      @@navigation += [parent]
    
      def navigation
        @@navigation
      end

    Finally, the view renders a partial with the data:

    
    
    <%= render :partial => 'shared/nav', :collection => controller.navigation %>

    With the partial template:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    
    <%
      if (nav.has_child_action_named? controller.action_name)
    -%>
        <li class="subsub"><%= link_to nav.name, {:action => nav.action_name}, {:title => nav.tooltip} %></li>
    <%= render :partial => 'shared/subnav', :collection => nav.children -%>
    <%
      elsif (nav.has_action_named? controller.action_name)
    -%>
        <li id="select"><%= link_to nav.name, {:action => nav.action_name}, {:title => nav.tooltip} %></li>
    <%= render :partial => 'shared/subnav', :collection => nav.children -%>
    <%
      else
    -%>
        <li><%= link_to nav.name, {:action => nav.action_name}, {:title => nav.tooltip} %></li>
    <%
      end
    -%>

    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

  26. Adam GrovesJune 07, 2007 @ 07:59 AM

    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:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    
    
    #Build main menu tab
    def link_to_main(name, options = {}, html_options = {}, *parameters_for_method_reference)
      if (options[:controller] == params[:controller])
        html_options[:id] = "active"
      end
      link_to(name, options, html_options, *parameters_for_method_reference)
    end
    
    
    #Build submenu tab
    def link_to_sub(name, options = {}, html_options = {}, *parameters_for_method_reference)
      if (options[:action] == params[:action])
        html_options[:id] = "active"
      end
      link_to(name, options, html_options, *parameters_for_method_reference)
    end

    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!)

  27. rdsJune 07, 2007 @ 01:03 PM
    I use something like this:-
    1
    2
    3
    4
    5
    6
    7
    8
    
    items_unless(true) do
      item 'Trip Listing', trips_path
      item_if admin?, 'New Trip', new_trip_path
    end
    items_if(true) do
      item 'Details', trip_path(@trip)
      item 'People', trip_users_path(@trip), :class => 'right'
    end
    It works pretty well using the link_to style.
  28. AndyVJune 07, 2007 @ 02:58 PM

    @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

  29. you can use a link_to_remote to retrieve in large data sets whose load you’d like to defer and use link_to_function if you have a small data set that can be put into the page.

    @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…

  • AndyVJune 07, 2007 @ 08:08 PM

    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.

  • aztaJune 08, 2007 @ 11:36 AM

    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

    1
    2
    3
    4
    
      <% tab :ponies %>
    
      <h1>This is your current list of Racing Ponies</h1>
      <p> ... </p>

    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:

    1
    2
    3
    
      def tab(name)
        @selected_tab = name
      end

    And my tabbed navigation partial defines the tabs and sets the ‘selected’ class on the @selected_tab:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    
    <% tabs = [ [:dashboard, dashboard_url], [:ponies, ponies_url], [:races, races_url]]
    
    <div id="pony_tab_navigation">
      <ul>
        <% tabs.each do |name, link| %>
    
        <%= content_tag :li, 
                         link_to(name, 
                                 link, 
                                 @selected_tab == name ? {:class => "selected"} : {}), 
                         :id => "#{name.to_s.downcase}-tab" %>
    
        <% end %>
      </ul>
    </div>

    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 :)

  • Gavin BakerJune 08, 2007 @ 12:11 PM

    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 ;)

  • Todd HussJune 16, 2007 @ 04:16 PM

    +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.

  • Jamie LawrenceJune 18, 2007 @ 04:13 PM

    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!

  • Ryan HeneiseJune 21, 2007 @ 06:26 AM

    Better late than never. Here’s what I use:

    1
    2
    3
    4
    5
    6
    7
    8
    
      # Controller
      class AboutController < ApplicationController
        before_filter :set_current_section
        
        def set_current_section
          @current_section = "about_us"
        end
      end
    1
    2
    3
    4
    
      # Helper: 
      def tab_for(text, link)
        content_tag :li, link_to(text, link, :class => ("selected" if @current_section.titleize == text))
      end
    1
    2
    3
    4
    5
    6
    
      # In the view: 
      <ul>
        <%= tab_for("Home", "/") %>
        <%= tab_for("Articles", "/articles") %>
        <%= tab_for("About Us", "/about") %>
      </ul>

    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.

  • JamisJune 27, 2007 @ 03:20 PM

    @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.

  • bitbutterJuly 18, 2007 @ 06:44 PM

    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} } %>
  • Rein HenrichsAugust 16, 2007 @ 10:59 PM
    My solution is a a version of Mr Eel’s, but I like to keep my classes and ids defined more semantically, so I in my rhtml I have:
    <ul class="<%= tab_class %>">
        <li class="tab1"><a href="">First Tab</a></li>
    </ul>
    and in my helper I have (say)
    def tab_class
      {
        "home" => "tab1",
        "foo"     => "tab2
      }[controller.controller_name]
    end
    That allows me to specify either simple controller-based tabs or a more complex organization scheme.
  • Mike StrambaAugust 25, 2007 @ 05:39 PM

    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

  • James Conroy-FinnFebruary 10, 2009 @ 10:57 PM

    ApplicationHelper: def tab_to(value, url) element_class = current_page?(url) ? ‘active’ : ‘inactive’ link_to value, url, :class => element_class end

    View: %li= tab_to ‘Home’, root_url %li= tab_to ‘Another’, another_url %li= tab_to ‘Link’, link_url

  • JoeJune 02, 2009 @ 02:50 PM
    1. application_helper.rb

    def build_navbar cur_ctrl, links items = [] links.each do |link| if link[:ctrl]===cur_ctrl # and link[:action]===cur_act items << content_tag(:li, link_to(link[:name], :controller => link[:ctrl], :action => link[:action]), :class => ‘highlighted’) else items << content_tag(:li, link_to(link[:name], :controller => link[:ctrl], :action => link[:action])) end end content_tag(:ul, items, :class => ‘navbar’) end

    1. _navbar.html.erb
    <% nav = [] nav << { :name => ‘Home’, :action => ‘index’, :ctrl => ‘splash’} nav << { :name => ‘Browse’, :action => ‘world’, :ctrl => ‘map’}
    1. etc %>
    <%= build_navbar controller.controller_name, nav %>
  • Comment