10 Jan, 2010

Published at 11:48PM

Tagged with code, golftrac, navigation, plugin, programming, projects, and rails

This post has 0 comments

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!

Comments

Do you have something to say about this post?
Protected by Defensio Textile Formatting Tips

or

Ryan Heath | Site Management A Ruby on Rails production.

This site is a Formed Function. Formed Function LLC | @formedfunction | Get in Touch