New and improved navigation gem for Rails.
Ruby
Switch branches/tags
Nothing to show
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Failed to load latest commit information.
lib
test
.gitignore
Gemfile
LICENSE
README.textile
Rakefile
navigation.gemspec

README.textile

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

Installation

Just add the gem to your Gemfile:

gem 'navigation'

And run bundle install.

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

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.

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.

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.

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

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

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.

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

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.

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.

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.

That’s all!

Feedback is welcome, and as always, feel free to fork and improve :-)