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!

Bharat Ruparel Sunday, 08 Aug, 2010 Posted at 04:44PM
Hello Ryan,
I have used your Navigation plugin in my open source project at: http://www.github/com/bruparel/file_manager
I am trying to upgrade to your current plugin so that I can move my project to Rails 3 eventually. My plan is to upgrade all the plugins, go to Rails 2.3.8 and then to Rails 3.
I have followed the instructions in your write-up here, but am having trouble coming up with an :admin menu block for the following admin menu snippet shown at the following URL:
http://github.com/bruparel/file_manager/blob/master/app/helpers/application_helper.rb
So I have created a symbol for the :admin menu and in my initializer block am trying to define it as follows:
See the client_perms line above? which corresponds to the code snippet:
So basically, I do not know how to tie a controller action to a menu item other than the default index action. If you need to see my menu in action, there is a link of the demo app deployed on Heroku on my home page.
Please advise on how to write this in your new plugin?
Thank you.
Bharat
Ryan Monday, 09 Aug, 2010 Posted at 09:17AM
Bharat -
With the old navigation plugin, you pass the items directly to the
navigationhelper (like you’re showing in yourx = ...code). However, with the new one, you define the menus in the initializer, and then you just pass the key (in your case,:admin) to thenavigationhelper.So to render you
:adminmenu, you would simply do this:This gives you even more flexibility to put logic around menus, because you could then define all of your menus in the initializer (in your case:
:admin,:leader,:eclient,:default) and simply render whichever one you need by passing the appropriate key.This would allow you to have a method, say, on your
Userobject that returned the role of the user and you would no longer have to worry about menu logic. For example:Then, in your layout (or wherever), you could just call:
And your menus would render automatically. Does that make sense? I think the big point of confusion for you is with the new plugin, you no longer pass each individual item to the
navigationhelper.Let me know how it goes.
Bharat Ruparel Tuesday, 10 Aug, 2010 Posted at 08:46PM
Hello Ryan,
Thanks for taking time out to respond to my queries quickly, I really appreciate it. Now that I have had time to play with your new plugin, I like it better already! I have got the basic menu going pretty much as you explain above. However, one thing I am not clear about. If you look at my ApplicationHelper class (http://github.com/bruparel/file_manager/blob/master/app/helpers/application_helper.rb), you will notice that I actually refer to the same controller (but different actions) from different tabs in the top level menu as shown below:
In here, you will notice that :client refers to ClientsController – index action. whereas: :client_perms refers to ClientsController – list_perms action.
How can this be achieved in your new plugin?
I can easily refactor my design and create another controller ClientPermsController and make the list_perms action the index action of the new controller, if necessary.
Please advise.
Thanks.
Bharat
Ryan Wednesday, 11 Aug, 2010 Posted at 09:14AM
Bharat -
In general, I think it’s a good practice to have clean, focused controllers, with only the 7 RESTful actions (if possible). When you start adding methods like “list_perms” and others, you run the risk of combining concerns and that’s not always good. So my recommendation, as you pointed out, would be to split that out into its own controller:
ClientPermsController. And make it the index action.Remember, if you’re using the new
navigationplugin/helper, you don’t pass the navigation items directly to the helper. Instead you define those in the initializer, and simply pass it the key that identifies the menu that was defined.Translating your code snippet above, you might have something like this:
Then, like I said before, you no longer need that complicated if/else series of menus in your application_helper.rb. All you need that method to do is return the key (i.e.
:adminor:leaderor whatever) based on the menus you’ve defined in the initializer file.Hope that helps.
Bharat Ruparel Thursday, 12 Aug, 2010 Posted at 10:51PM
Hello Ryan,
Thanks for your response and advice. I have separated ClientPermsController and FolderPermsControllers out of ClientsController and FoldersController respectively. So that has worked out really well.
I cannot however, figure out the last part, that is: the admin section. If you look at my online demo running at Heroku: (username/password – system/important).
http://empty-fog-47.heroku.com/
Then you will see that it has an admin section which is an empty menu rendering the next level view! I had copied your old plugin’s navigation methoed and created sub_navigation method with minor changes for the second level navigation.
So I tried to adapt you new plugin’s model and created sub_navigation method by copying navigation and changing it slightly. You can see it in my code on the github.
How can I use the new plugin to emulate the second level menu? I tried the nested menu in your new plugin but that breaks my layout completely.
So I created another named menu in the initializer as follows:
And tried to connect it to the :admin menu defined above it:
The routes file defines a named menu as follows:
Now, in the application layout, I call sub_navigation directly as shown below:
.block .secondary-navigation = sub_navigation .clear .contentBut that gives me a sub menu which has the main menu items tacked on to it! Have you tried to create second level menus like this? Please advise. This is the last hurdle to overcome for me in upgrading to your new plugin.
Thanks.
Bharat
Bharat Ruparel Saturday, 14 Aug, 2010 Posted at 10:28AM
Hey Ryan,
I am all set. Please ignore my previous post. Everything works as intended with your new plugin. Thanks for the excellent work. I would like to collaborate with you on some projects if we can.
Regards, Bharat
Bharat Ruparel Sunday, 15 Aug, 2010 Posted at 02:36PM
Hello Ryan,
I have successfully upgraded my application to Rails 2.3.8 but have run into problems with Rails 3 rc upgrade. Here is the error that I am getting:
ActionView::Template::Error (undefined method `navigation' for #<#<Class:0x00000003137f88>:0x00000003131818>): 3: - elsif (is_leader? || is_staff?) 4: = navigation :leader_or_staff 5: - else 6: = navigation :welcome app/views/shared/_main_navigation.html.haml:6:in `_app_views_shared__main_navigation_html_haml___1653348847369200762_24408400__302207085200435719'Have you used your plugin with Rails 3 yet?
Please advise.
Thanks.
Bharat
Ryan Tuesday, 17 Aug, 2010 Posted at 10:02AM
Bharat -
Sorry, but no, I haven’t. I plan to soon, though, and I will let you know as soon as I can. I can’t think of any reason it wouldn’t be recognized from the view in Rails 3. I’ll let you know once I know.
Thanks.