Free-for-all: Tab Helper (Summary)

Posted by Jamis Thursday, June 28, 2007 02:39:00 GMT

The first RailsWay free-for-all came off quite well. Many of you posted your favorite solutions to the problem of tab-based navigation, as posed by Nate Morse.

Jamis’ Take

Of all the solutions posted, my personal favorite was the pragmatic and simple CSS-based solution given by Mr. eel (Nate Morse came to the same solution independently):

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

What a great approach. Although I would make the choice of the body ID explicit (rather than depending on the controller name), it is otherwise really nice. It shrugs off the whole issue of “should the current tab be a link” by saying it just doesn’t matter—every tab is always a link. Such pragmatism gets right to the heart of the Rails Way: implement just what matters, and nothing more.

Koz’s Take

A number of solutions relied on tightly coupling the controller and tabs. While this may seem like a time-saver at first, I believe that it’s unlikely to remain useful as your application grows. You’ll find yourself moving functionality into strange locations in order to make your tabs highlight correctly.

The problem is amplified with a restful application where your choice of controllers are dictated by the resources that you’re managing. You may have a list of comments in several different sections of your application, but not want to highlight the ‘comment’ tab whenever you display them.

Personally, I prefer the really simple approach of a before filter and a navigation partial.

1
2
3
4

def set_current_tab
  @current_tab = :people
end

Thanks, everyone for your submissions!

Comments

Leave a response

  1. Nico OrellanaJune 28, 2007 @ 05:47 AM

    I had the same issue as Nate and just five minutes ago I thought to myself… humm tabs… humm the guys from RailsWay came with some useful stuff … and I just open my rss reader and vuala … the rails way. Cheers guys!

  2. NeoMikeJune 28, 2007 @ 03:10 PM

    After looking at a lot of the comments submitted in the original post, I have no doubt why so many Rails Apps are slow.

  3. Glenn BeckertJune 28, 2007 @ 07:33 PM

    As someone struggling to “get” rails, I’d love to see a working example, if anyone has one to show.

  4. Jeff GillisJune 29, 2007 @ 12:46 AM

    This solution is also posted in the book titled CSS Mastery: http://www.friendsofed.com/book.html?isbn=1590596145

    It is a great resource for all CSS related design needs, and has helped me many times.

  5. Luke MonahanJuly 02, 2007 @ 01:48 AM

    Jamis’ solution really did seem good idea to me conceptually at first, but thinking about it further I’m not so sure. The content is linked to the presentation, as the css now has to know that “users”, “comments”, “posts” etc. exist.

    Has the concept of separation of style from content been hammered home so hard that I now can’t see the forest for the trees? Or is using this method likely to be an actual problem in future development, or an accessibility issue?

    Cheers,

    L.

  6. Jon StenqvistJuly 04, 2007 @ 09:10 AM

    I just found this post to late, when search for good solution for TAB. This is what i came up with my self. This way use span if the current page is selected.

    First of all i created a Object that would hold my sitemap.

    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
    
    
    class MainMenu
      
      attr_accessor :children, :name, :options, :visible
      
      def initialize(name, options = {}, &block)
        @children = []
        @name = name
        @options = options
        @visible = true
        yield(self) if block_given?
      end
      
      def add_child(item, &block)
        yield(item) if block_given?
        @children << item
      end
      
      def has_children?
        # TODO: kolla om det finns synliga childrens
        @children.length > 0        
      end    
        
      def is_selected?(options = {})
        return true if @options[:controller] == options[:controller] && @options[:action] == options[:action]    
        for child in @children
          return true if child.is_selected?(options)
        end    
        false
      end
        
    end
    

    Then I created a method in application.rb where we build the menu depending on logged on user and the roles. (This could also be an ApplcationHelper method.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    
    
    def navigation
        @navigation ||= MainMenu.new("root") do |r| 
        r.add_child MainMenu.new("Inställningar", :controller => 'account', :action => 'edit') do |s|
          s.add_child MainMenu.new("Ändra lösenord", :controller => 'account', :action => 'password')
          
          if logged_in? && self.current_user.is_in_role?('admin')
            s.add_child MainMenu.new("Användare", :controller => 'account', :action => 'users')
            s.add_child MainMenu.new("Rättigheter", :controller => 'account', :action => 'userroles') {|item|item.visible = false}        
          end
        end    
        r.add_child(MainMenu.new("Hästar", :controller => 'horses', :action => 'list')) do |s|
          s.add_child MainMenu.new("Sök hästar", :controller => 'horses', :action => 'search')
        end
      end
    end
    

    The partial rendering on the menu. I render the menu in application.rhtml

    1
    2
    
    
    <%= render :partial => 'shared/mainmenu' %>
    

    Notice the controller.navigation to be able to get the public method navigation from the application controller that is inherited in the controller.

    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
    
    
    <div id="header">
        <ul id="primary">
      <% for child in controller.navigation.children %>
         <% if child.is_selected? :controller => controller.controller_name, :action => controller.action_name -%>
            <li><span><%= child.name %></span>
         <% if child.has_children? -%>
                <ul id="secondary">
          <% for subitem in child.children -%>
          
          <% if subitem.visible -%>
                    <li>
                    <% if subitem.is_selected? :controller => controller.controller_name, :action => controller.action_name -%>
                        <span><%= subitem.name %></span>
                    <% else %>
                        <%= link_to subitem.name, subitem.options %>
                    <% end %>
                    </li>
                <% end %>
                
                <% end %>
                </ul>         
           <% end %>
            </li>
         <% else %>
            <li><%= link_to child.name, child.options %></li>
         <% end %>
        <% end -%>
        </ul>
    </div>
    
  7. NeoMikeJuly 05, 2007 @ 07:57 PM

    Thus proving my point from above.

  8. OldMikeJuly 05, 2007 @ 08:52 PM

    @NeoMike:

    Instead of just being grumpy, please contribute something and explain why.

  9. NeoMikeJuly 06, 2007 @ 12:00 AM

    @OldMike

    You’re right, I am grumpy, and I apologize.

    I’m grumpy because I’m tried of having to work on other people’s code that is over-engineered, and it’s not just a Rails problem. Developers (for various reasons) over-engineering their code. Then people turn around and while that things are so damn slow, complex, or hard to maintain. This isn’t a new problem.

    The CSS based solution listed in this post is a great example of how to tabs in a simple and elegant way and it would require only a single line of ruby (such as [%= params[:controller] %] )to put into a generic layout.

    With the comment above as the exception (it support sub-menus – yes I was being grumpy), many of the tab implementations put into the original post performed the same functionality, but in dozens of lines of code.

    Lets see… dozens of lines of code vs. one. Which one will be easier to troubleshoot? Which one will be higher performance?

    Nobody is perfect, and we all need to learn something new. Next time you write a piece of code, stop and think about if there is a more pragmatic way to implement it.

    I’m just being exceptionally grumpy lately because I’m stuck maintaining a rails application that is about 100% over engineered. I’m slowly re-writing it and reduced the code-size by about 20% so far while maintaining functionality.

    Since the code is internal, I can’t post an example, but I can say that behaviour.js really does a bang up job in reducing the amount of embedded javascript hooks on an ajax enabled application. Also helps make the HTML so much more readable.

    Moving a lot of logic from several controllers into the single model where it belongs also helped reduce a lot of redundant code.

  10. Jon StenqvistJuly 07, 2007 @ 09:17 AM

    Sorry NeoMike, I didn’t realize that my solution was so over engineered. There is no more functionality then I need added. I just came form C# och Delphi development and I can tell that my solution here is less code then just my xml sitemap file was i ASP.NET ;)

    I often see ruby code that is not well design but from a OO perspective, that code is properly the code you call not over-engineered.

  11. Nick CaldwellJuly 09, 2007 @ 05:57 AM

    The CSS-based approach isn’t ideal (though pretty much everyone uses it) from a usability perspective. It’s just bad to have a live link (even one hidden by styling) pointing to the page it’s on. It breaks user expectation about how links work and think how confusing it could potentially be for a user with visual disabilities.

  12. JamisJuly 09, 2007 @ 09:52 PM

    @Nick: actually, it doesn’t break expections at all. It’s still a link. You click on it, and you go where the link points to (which is here). Lots of sites do this, even big prominent ones (go to http://www.ibm.com and click the IBM logo, for instance). And I don’t see how that could be any more confusing for a visually disabled person than for a sighted person. The screenscraper reads through the links and includes one that points to the current page. Not a big deal.

  13. SubbuJuly 11, 2007 @ 01:03 PM

    In Mr. eel’s method how does the current tab highlighted? Suppose if I have a tabbed menu which updates some parts of the page with a XHR call how do we handle the highlighting? In this case the menu portion is never updated.

  14. Jason RoelofsJuly 12, 2007 @ 03:04 PM

    I just use the TabNav plugin. It works amazingly well http://blog.seesaw.it/pages/tabnav.

    I do like the simplicity of the CSS solution, but I also agree with the concern of separation.

  15. MancyJuly 16, 2007 @ 06:07 PM

    Thank you all for your ideas

    my way of doing it, is like Mr. eel but with dynamic way and for many tabs under one action.

    TestController
    1
    2
    3
    4
    5
    6
    7
    
    
    def action_with_many_tabs
        # id is a label name
        @current_id = params[:id] || @Model.find(:first).label
        return false unless request.post?
        #your action code here
    end
    
    in action_with_many_tabs.rhtml
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    
    
        <style type="text/css" media="screen">
        #anything #<%= @current_id %> {
            background:red;
            font-weight:bold;
        }
        </style>
    <div id="anything">
            <ul>
                <% @models.each do |model| -%>
                    <li id="<%= model.label %>"><%= link_to model.label, :action => "action_with_many_tabs",:id => model.label %></li>
                <% end -%>
            </ul>
        </div>
    

    that’s all, sure you’ve to apply your special CSS style it’s just an example so, when @current_id equals model.label the effect will appear on this list element.

    enjoy!

  16. Nick CaldwellAugust 01, 2007 @ 08:28 AM

    @Jamis:

    It’s probably not something that would throw an experienced user off, but it does break expectations—we expect that links take us somewhere other than where we are right now. If the expectation is weakening because it’s so often violated now, it’s a shame, because it’s a useful expectation to have.

    Jakob Nielsen talks about some related issues here: http://www.useit.com/alertbox/within_page_links.html

  17. linojAugust 02, 2007 @ 07:32 AM

    Great discussion, here’s what I ended up doing: http://www.vaporbase.com/postings/A_tab_helper_that_works_for_me

  18. Andrew HobsonSeptember 04, 2007 @ 08:04 PM

    This won’t work in all situations, but I think it works nicely for simple tab navigation. It merges several ideas I’ve seen around. Obviously you can change the generated HTML to use a span instead of a link if you are so inclined.

    In application_helper.rb:
    1
    2
    3
    4
    5
    6
    7
    8
    
    
        def layout_link_to(link_text, path)
          curl = url_for(:controller => request.path_parameters['controller'],
                                :action => request.path_parameters['action'])
          html = ''
          options = path == curl ? {:class => 'current'} : {}
          html << content_tag("li", link_to(link_text, path, options))
        end
    
    In layout.rhtml:
    1
    2
    3
    4
    5
    6
    
    
        <ul>
          <%= layout_link_to "Users", users_path %>
          <%= layout_link_to "Comments", comments_path %>
          <%= layout_link_to "Recent Posts", recent_posts_path %>
        </ul>
    
  19. David JamesSeptember 23, 2007 @ 03:17 PM

    I am torn! I like the simplicity and compactness of the recommended approaches.

    However, I prefer TabNav (part of the Widgets plugin). I like having one file declaratively handle my tabs, what they say, what they link to, and when they highlight.

    Take a look and judge for yourself: http://blog.seesaw.it/articles/2007/09/03/what-changes-in-the-new-widgets-tabnav

    If someone could cook up a solution with the same advantages as Tabnav (a sweet DSL, one declarative file to describe the tabs’ behavior) with the compactness of the Mr. eel-like solution, I would like to see it!