Multi-menu tabbed navigation in Rails
I just recently scrapped my old navigation plugin (in several applications) in favor of the new one I wrote. It has allowed me to clean that part of my code up tremendously. I know I’ve posted about it already, but I thought I’d show a real-world example of how I’m using it in Golf Trac.
First, here’s the situation. There are three main areas where navigation comes into play: 1) public navigation 2) sidebar navigation and 3) action-level sub navigation. Here’s what I’m referring to:
Not logged in (1):

Logged in (2 and 3):

The public and sidebar navigation doesn’t really change. But the tabs at the top right (sub navigation) change depending on where you are in the app, as they allow you to navigate to different actions within the same controller.
So here’s how it works. I define my navigation menus in an initializer and reference them by a key. This keeps my controllers and views clean. Also, the new plugin has native support for nested menus and action-level navigation, which is something the old one just wasn’t designed to support.
Here’s a snippet of the actual menu definitions used in Golf Trac.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | # config/initializers/navigation.rb RPH::Navigation::Builder.config do |navigation| navigation.define :public do |menu| menu.item WelcomeController, :path => :hello_path menu.item TourController, :text => 'About & Tour' menu.item PlansController, :text => 'Plans & Pricing' end navigation.define :sidebar do |menu| menu.item DashboardController menu.item RoundsController menu.item CoursesController menu.item FriendsController, :text => Proc.new { |view| view.sidebar_friends_text } menu.item SettingsController menu.item AdminController, :text => 'Administrator', :path => :administrator_path, :if => Proc.new { |view| view.logged_in_as_admin? } end # ... end |
As you can see, I’m defining a menu for each of the different situations. You can define as many as you want. It’s worth noting that the order that you list your menu items is the order they’ll be rendered.
So once your menus are defined, use <%= navigation :public %> to render them (:public being the key/name of a menu).
A nice side-effect about this is there’s no sloppy ERB tags to deal with. This is a Ruby file and allows for a natural use of Ruby blocks. That in itself is an improvement. Also, the plugin allows you to actually pass the controller itself as a menu item, rather than ensuring you have proper string matching. As expected, a RoundsController would provide a “Rounds” link that goes to the rounds_path. But as you can see, if you need to override the text or the path, that’s not a problem.
One interesting thing I’d like to point out is the “Friends” link in the sidebar navigation. Looking at the second screen shot above, see how it has that little number (in this case a “1”) next to the text? That indicates that you have 1 new friend request. But more importantly, that also means that the text for that item will be dynamic. Fortunately, that’s not a problem.
If you pass a Proc to the :text option, you will be handed an instance of the view. From there, you can call any helper your heart desires. Here’s what Golf Trac does:
1 2 3 4 5 6 7 8 | def friend_requests_count return if (requests = current_user.friend_requests).size.zero? content_tag :span, requests.size end def sidebar_friends_text ["Friends", friend_requests_count].compact.join(" ") end |
So when you see this:
1 2 | menu.item FriendsController, :text => Proc.new { |view| view.sidebar_friends_text } |
It’s just a simple helper call.
In a similar fashion, you can determine which menu items should be shown based on the result of some condition. In this case, I have an “Administrator” section that should only be shown if an administrator is logged in. So that’s what this does:
1 2 3 | menu.item AdminController, :text => 'Administrator', :path => :administrator_path, :if => Proc.new { |view| view.logged_in_as_admin? } |
Notice the :if option, here. And that’s just another helper call to:
1 2 3 | def logged_in_as_admin? !!current_user.administrator? end |
Simple stuff, but it makes a world of difference when you’re trying to hack together some navigation with different rules, conditions, text, paths, etc.
Now, that leaves one final question: how is the action-level sub navigation handled? It’s pretty simple, too. When you’re defining a menu, you can specify that it’s an :action_menu, which simply means that it will check against the current action rather than the current controller.
Note: if you want nested navigation, just pass a block to the menu.item call, defining the menu you want nested. In this case, we don’t want a nested menu, but still want action-level navigation. See the README for more.
So all I do is define the menus I want for each controller, using a key of that controller’s name. Here’s the sub navigation for the RoundsController, for example:
1 2 3 4 5 | navigation.define :rounds, :action_menu => true do |menu| menu.item :index, :text => 'List', :path => :rounds_path menu.item :best, :path => :best_round_path menu.item :new, :path => :new_round_path end |
You can name the action-level menus anything you want, but by naming them after the controller in which they correspond, you can do some automatic rendering. For example, here’s what I’m doing:
1 2 3 4 5 | def sub_navigation navigation controller_name.downcase.to_sym, :class => 'sub_navigation' rescue RPH::Navigation::InvalidMenuIdentifier nil end |
If there’s no menu defined, nothing will happen. You can stick a call to the <%= sub_navigation %> helper in your layout, and it will work automatically. Nothing in your controllers, and no jumbled view code.
Nearly every Rails application has navigation. It’s something you have to deal with over and over again, too. Hopefully you have realized how something like this navigation plugin can greatly help you organize almost any form of navigation in your Rails app.
To install:
1 | $ script/plugin install git://github.com/rpheath/navigation.git |
Happy coding!
