New and improved navigation gem for Rails.
h1. Navigation for Rails
This is an experiment with a different way to create navigation menus in Rails. It supports navigation at the controller level, as well as the action level (for both nested or non-nested menus).
h2. Installation
Just add the gem to your Gemfile:
gem 'navigation'
And run @bundle install@.
h2. Initialization
Probably the most fundamental difference with this gem and most navigation gems is this one requires the menus to be pre-defined before rendering. Here’s an example of a typical menu definition (in @config/initializers/navigation.rb@):
RPH::Navigation::Builder.config do |navigation|
navigation.define :primary do |menu|
menu.item :home, :text => 'Welcome'
menu.item :about, :path => :about_us_path
menu.item :contact, :text => 'Contact Us'
end
end
A caveat to this approach is that you don’t have access to the named route helpers in an initializer, so you have to pass them as either a symbol or a string and they’ll be eval’d at the right time. If you don’t pass a route (i.e. the @:path@ option), it will try to do @“#{menu_item_name}_path”@. So the “home” item in the above example would get a path of “home_path” since there was no @:path@ option. And if “home_path” doesn’t exist, the fallback is “root_path”.
h2. Rendering the Menu
Once you have your menu(s) defined, it’s pretty easy to render it:
<%= navigation :primary %>
You just have to pass the key of the name you gave it when it was defined. This is good for a number of reasons, but an added bonus is the ability to define and render different menus depending on who is logged in, for example.
<%= navigation current_user.menu %>
All of the logic related to roles/permissions could be abstracted into a @menu()@ method. Or if you’re funny about sticking that logic in a model, you could call a helper or something. But you get the idea: key-based menu identification.
h2. Tips & Advanced Features
There are some additional things that you may want to know about this gem, so keep reading if you’re not satisfied yet.
h3. Passing a controller instance…
When I’m writing code to determine the “current tab” stuff, I basically just want this: when any action inside of the @HomeController@ gets rendered, highlight the “home” tab as the current. So why not allow the ability to pass the controller instance in the menu definition?
RPH::Navigation::Builder.config do |navigation|
navigation.define :primary do |menu|
menu.item HomeController, :text => 'Welcome'
menu.item AboutController, :path => :about_us_path
menu.item ContactController, :text => 'Contact Us'
end
end
No more guessing games and no more @current_tab :home@ in your controllers.
h3. Rule-based navigation menus…
Sometimes you only want to show a menu based on a some condition. It happens. Just pass an @:if@ condition to the menu definition:
RPH::Navigation::Builder.config do |navigation|
navigation.define :primary, :if => Proc.new { |view| view.allowed_to_show_menu? } do |menu|
# ...
end
end
You will automatically be handed a reference to the template/view so you can call any method that @ActionView@ is aware of (Note: if you need the controller, just use @view.controller@).
h3. Rule-based navigation menu items…
And sometimes you always want the menu present, but one or two of the tabs should only show up for certain reasons. Well, the @:if@ option works exactly the same for menu items as well:
RPH::Navigation::Builder.config do |navigation|
navigation.define :primary do |menu|
menu.item :home, :text => 'Welcome'
menu.item :about, :path => :about_us_path
menu.item :admin, :if => Proc.new { |view| view.logged_in_as_admin? }
end
end
h3. Sub-menu navigation…
As we all know, a controller is made up of actions. And sometimes you want to not only provide navigation around your controllers, but also around the actions within a controller. Well, it’s a pretty simple concept and now has a pretty simple solution. Just pass a block to the menu item:
RPH::Navigation::Builder.config do |navigation|
navigation.define :primary do |menu|
menu.item :home, :text => 'Welcome' do |sub_menu|
sub_menu.item :index, :text => 'Dashboard', :path => :home_path
sub_menu.item :friends, :path => :friends_path
end
menu.item :about, :path => :about_us_path
menu.item :contact, :text => 'Contact Us'
end
end
If you pass a block to any of the menu items, the gem will automatically know that this is a sub-menu and will render a completely new menu underneath of the parent menu item. And it’s “current tab” will be based on the @action_name@ instead of the @controller_name@. Remember, this is if you want a nested action-level menu.
h3. Action-level navigation…
If you want to build a separate menu for the actions of a controller, but you don’t want to nest it under a parent menu item, you can tell the builder that during the definition by passing @:action_menu => true@:
RPH::Navigation::Builder.config do |navigation|
navigation.define :users, :action_menu => true do |menu|
# ...
end
end
h3. Multiple menus…
This isn’t a feature per se, but one of the great outcomes of this experiment is the ability to keep all of your menu definitions in a single ruby file and in a single builder…
RPH::Navigation::Builder.config do |navigation|
navigation.define :public do |menu|
# ...
end
navigation.define :authenticated do |menu|
# ...
end
navigation.define :administrator do |menu|
# ...
end
end
Having key-based menu identification works out really well for additional menu logic and keeping your views clean.
h3. Helper-based custom menu text
Sometimes the text on one of your menu items depend on some conditions. Like, for instance, maybe an inbox link might also show the number of new messages? You could define a helper like so:
def inbox_menu_text
["Inbox", content_tag(:span, current_user.messages.unread.size]].join(" ")
end
Then you can refer to that helper in the navigation definition using a Proc.
navigation.define :authenticated do |menu|
menu.item :messages, :text => Proc.new { |view| view.inbox_menu_text }
end
This works for both controller and action-based menus.
h3. Manually overriding current sections
This doesn’t work for action-based menus, but in controllers you can override the current section via the @current_section()@ method. For example:
class Admin::UsersController < ActionController::Base
current_section :administrator
end
This is really only useful for controllers that don’t have an explicit menu item, but you would still like one of the sections to be highlighted.
h3. That’s all!
Feedback is welcome, and as always, feel free to fork and improve 😃