Rails plugin: navigation helper

This plugin is now available at GitHub. Follow all plugin modifications there (the SVN repo will soon be obsolete).

It seems like I’ve been going on and on about navigation in Rails lately. That’s probably because I have been. But it’s one of those things that presents itself in every app I work on. And since I usually go about it close to the same way, it makes a good case for a plugin, don’t you think? I shall call it, navigation helper. Clever, I know.

It’s pretty basic, but I’ll go through a few examples, anyway. Before I continue, two notes about these examples:

  1. whenever you see navigation(...), think of <%= navigation(...) -%> as that method is a helper used in a view
  2. assume the current page will always be ‘Home’

Now that we have that settled, let’s see a few examples.

Basic Usage

This is how most people would probably use this plugin:

1
2
3
4
5
6
7
8
navigation [:home, :about, :contact_me]

# HTML output:
# <ul class="nav_bar">
#   <li class="current"><a href="/home">Home</a></li>
#   <li><a href="/about">About</a></li>
#   <li><a href="/contact_me">Contact Me</a></li>
# </ul>

You must use symbols as the links/sections, and we’ll see why in just a bit. Also, notice how :contact_me turns into ‘Contact Me’ as link text. You can use CSS to transform it to all lowercase or all uppercase, but the default is to capitalize each word (which you can’t do with CSS).

Oh, and the above example would be looking for the following named routes: home_path, about_path, and contact_me_path. So make sure you have a named route for each section you pass to the helper.

Authorized Sections

I don’t know about you, but a lot of the time I have one or two tabs that only appear based on some sort of authorization. For example, this very site has ‘Portfolio’, ‘Words’, ‘Archives’, and ‘About’ at the top. But if I’m logged in it also has ‘Admin’. Again, that’s a common need for me, so I added in support for that. Use it like so:

1
2
3
4
5
# Example 1: single section
navigation [:home, :about, :admin], :authorize => [:admin]

# Example 2: multiple sections
navigation [:home, :about, :users, :reports], :authorize => [:users, :reports]

The plugin will only add those sections to the list if they are authorized. And how does it “authorize”? Well, by default the plugin will check against a logged_in? method. If that method either doesn’t exist or fails (returns false), those tabs will not be added. “But Ryan, don’t you think it’s kind of wrong of you to assume that we’re using a logged_in? method?” Well, maybe, maybe not. Either way, you can override that by telling the helper which method to use, like so:

1
navigation [:home, :about, :admin], :authorize => [:admin], :with => :auth_method

Now auth_method will be checked instead of logged_in?.

One more thing. In the case that the entire navigation is to be authorized, there is a way to avoid re-typing all of those sections in :authorize option.

1
navigation [:home, :about, :admin], :authorize => [:all]

Adding Subtitles

This was a recent need of mine (in the Portfolio). What I mean by subtitles is text that supports each link, but is not part of the link itself. That may have not helped you at all. Maybe this example will:

1
2
3
4
5
6
7
8
9
10
11
12
13
navigation [:home, 'Start Here', :about, 'Learn More']

# HTML output:
# <ul class="nav_bar">
#   <li class="current">
#     <a href="/home">Home</a>
#     <span>Start Here</span>
#   </li>
#   <li>
#     <a href="/about">About</a>
#     <span>Learn More</span>
#   </li>
# </ul>

Then you can do some fancy CSS :hover styling or whatever. Sometimes that adds just enough spice to a boring navigation bar.

But wait, there’s a second option. If you want the subtitles to appear as hover text instead, just let the helper know:

1
2
3
4
5
6
7
navigation [:home, 'Start Here', :about, 'Learn More'], :hover_text => true

# HTML output:
# <ul class="nav_bar">
#   <li class="current"><a href="/home" title="Start Here">Home</a></li>
#   <li><a href="/about" title="Learn More">About</a></li>
# </ul>

And as I briefly mentioned above, that is why you have to pass symbols as the link/section and strings as the subtitles. The plugin understands symbols and strings differently (the positions matter, too: even for sections, odd for subtitles).

Setting Current Tab

OK, the last thing. By default the plugin uses the controller’s name to determine the “current” tab (or link or section) you’re on. But since that’s not always feasible, you can specify the current tab for any controller by doing:

1
2
3
class PublicController < ApplicationController
  current_tab :home
end

Now something like this will work as intended:

1
navigation [:home, :about, :contact_me]

The PublicController will be seen as :home to the navigation helper, and will choose the current tab accordingly.

Documentation

All (or most) of the above is in the README. It’s on the agile web development site, too (although it doesn’t provide much info).

Installation

If you think this plugin could help you out, by all means install it. Here’s how:

ruby script/plugin install http://svn.rpheath.com/code/plugins/navigation_helper

...or…

piston import http://svn.rpheath.com/code/plugins/navigation_helper vendor/plugins/navigation_helper 

Conclusion

I have a few more things I want to do with this plugin (such as sub-navigation), but I didn’t want to get unnecessarily complex just yet. Feel free to modify the code to your needs, and let me know if there’s something you absolutely think should be added/changed/fixed. Who knows, I just might do it! Enjoy.

Comments

01

Aaron H. on Tue Feb 19 at 08:35AM

This is absolutely great. I find myself doing the same thing over and over. Not sure why it didn’t occur to me that this would be a good plugin.

I haven’t checked out the code yet or anything, but the idea itself seems great. I love that you even thought to include the authorized tabs and active tab overrides.

Thinking about this (again, without looking at code and only thinking about future expansion), it might make sense to have the primary navigation array also be hashes. i.e.

1
2
navigation [:home => {:tip => 'Link to home page', :hover => 'Start Here'}, 
:about => {:tip => 'More about us', :hover => 'Learn More'}]

I love your concise syntax, but having an option of something like this could really open up the flexibility. The best thing about this would be the potential to do nested drop down menus (which I tend to need more often than not) with something like:

1
2
navigation [:home => {:navigation => [:photos, :posts, :settings]}, 
:about, :admin => {:navigation => [:moderate, :users]}]

Again, this is not at all a criticism. I’m just so excited by the idea that I can’t stop making it more and more complicated. It’s a problem I have. :)

02

Ryan on Tue Feb 19 at 01:49PM

You know, I was back and forth on the the hash or array thing. My original plan was (and this was concerning subtitles, but use your imagination) to do something like this with the subtitles:

1
navigation({:home => 'Start Here', :about => 'Learn More'})

The section/subtitle association is a lot more obvious that way. But there’s a problem with that: a Hash is an unordered container. So then I thought, no biggy, if a Hash is passed in, I’ll just instantiate a new dictionary object, which will preserve the order.

Well, I would have had to do something like this:

1
2
3
4
sections = Dictionary[:home, 'Start Here', :about, 'Learn More']

# then this would work...
sections[:home] # => 'Start Here'

But the fact that I had to still pass section, subtitle, section, subtitle to the dictionary class (same as the array) sort of turned me off to that idea. In the end, I decided to just use a plain array instead of an array/hash combo.

Personally, I don’t think visible subtitles AND hover text is applicable. It seems like at that point I’d be adding options just for the sake of adding options. But yes, this plugin could definitely be extended a bit more to cover all sorts of other navigation needs.

03

Aaron H. on Tue Feb 19 at 07:18PM

I get your point about needing both as being redundant, I was just trying to make it as ridiculous as possible.

WRT to hashes, in Ruby 1.9 all hashes are ordered and in Edge Rails there is an OrderdHash class included in active support.

http://www.culann.com/2008/01/rails-goodies-activesupportorderedhash

04

jDeppen on Thu Feb 21 at 07:04PM

Great plugin!

I was glad to see that it uses class rather than id. I needed more than one so I did something like this:

1
2
navigation [:first, :second], :authorize => [:second]
navigation [:third, :fourth], :authorize => [:third], :with => :some_auth_method

I didn’t see any support for link_to_unless_current did I miss it? I did notice class=”current” for styling.

Thanks for the plugin.

05

Ryan on Fri Feb 22 at 03:43AM

Well, I’m of the opinion that there’s no need to replace the link with text on the “current” tab. Also, since this plugin matches the current tab at the controller level, several actions could result in that controller being the “current” one. I like to be able to always click the tab again to get to the default page, rather than providing a series of breadcrumbs to backtrack.

But it wouldn’t be all that hard to modify the plugin itself, and feel free to do so. You could also use javascript to do this:

////
// prototype
document.observe("dom:loaded", function() {
  $$('ul.nav_bar li.current').each(function(e) {
    e.update(e.down().innerHTML);
  });
});

////
// jQuery
$(document).ready(function() {
  $('ul.nav_bar li.current').each(function(i) {
    this.html(this.children('a:first').text());
  });
});

That’s untested, of course. Admittedly, though, I’d modify the plugin over a javascript solution, I just thought I’d mention it.

Oh, and I’m glad you found the plugin useful :-)

06

Jörg Battermann on Wed Mar 05 at 02:05AM

Hello there,

thanks for the useful plugin! Quick question: is it possible to add sub-elements of a navigation element like e.g. in this example:

http://qrayg.com/experiment/cssmenus/

So you can specify sub-navigation elements of each of the navigation [:home, :about, :admin].. elements… or no subelements and the behaviour is just like the current one.

Hope that makes sense :)

-J

07

Jörg Battermann on Wed Mar 05 at 02:13AM

Well even better / compliant: http://www.htmldog.com/articles/suckerfish/dropdowns/

08

Ryan on Wed Mar 05 at 02:59AM

Sorry, no sub-elements for the navigation helper. It’s a different type of navigation from a menu-driven approach. It provides more of a tabbed interface. But maybe that’s an idea for a navigation_menu plugin? I could actually use that on a project I’m working on… so who knows… maybe I’ll write it (I’ll post about it here if I do).

09

Jörg Battermann on Wed Mar 05 at 04:31AM

Ryan,

alright – thanks for letting me know. I need it kinda right away, too … and will implement it one way or another over the next couple days. In case you wanna do it together, let me know :)

-J

10

Eric Morand on Wed Mar 05 at 07:36AM

Hi Ryan,

I keep on having “undefined method `home_path’ for #” error when I add you navigation bar to my home page.

Am I doing something wrong here ?

<%= navigation [:home, :about, :contact_me] %>

I have added the following lines in my routes.rb file :

1
2
3
map.home_path 'home', :controller => 'home'
map.about_path 'home', :controller => 'home'
map.contact_me_path 'home', :controller => 'home'

Any idea ?

11

Ryan on Wed Mar 05 at 08:04AM

Yeah, minor problem with your routes. You need:

1
2
3
map.home       '/home',    :controller => ...
map.about      '/about',   :controller => ...
map.contact_me '/contact', :controller => ...

From that, Rails will build helpers for you based on the map.[whatever] part. So for map.home you’ll get home_path, home_url, etc. In your case, though, you’re getting home_path_path because you’re including “_path” in the route definition. Try omitting the “_path” part in routes.rb and see if that helps.

Let me know.

12

Eric Morand on Wed Mar 05 at 10:31PM

Thanks Ryan, it solved my problem. I did know that Rails was building _url helpers but not _path ones.

That works great, thanks for the support and the plugin !

13

kris on Thu Mar 06 at 02:58PM

Ryan, what about situations where you need to pass a parameter to the named route. For example:

user_path(@user)

How would it work here?

14

Ryan on Fri Mar 07 at 08:38AM

Kris -

For my needs, I usually have the tab mapped to a controller’s index action (since it’s the default), which can act as sort of a “dashboard-ish” type of representation (for users it may be the most recently registered users, or whatever). So in your example, I’d have the tab be :users (which would map to the default index action of the users_controller), and use current_user (either via restful_authentication or my own implementation) to represent anything I needed to do that was user-centric.

But I understand that you may have just been using that as an example, and the user you’re referencing above may not be yourself (ie, current_user). In either case, I think that may be getting a little too specific for the plugin, but feel free to hack away if you want to add support for that type of thing.

15

Jason on Tue Apr 22 at 10:32PM

Hi,

I would like to specify the text of the link instead of the default of the section name. Will appreciate any help on how to tweak this plugin to achieve this.

thanks.

16

Jason on Tue Apr 22 at 11:25PM

Never mind,

I managed to do what I wanted. Basically in the construct method where the subtitles are added to the link I am doing

link = link_to(content_tag(:span, SUBTITLES[section]), send("#{section.to_s.downcase}_path"), :title => SUBTITLES[section])

therefore getting the span tag as part of the link which is what I needed.

Cheers!.

17

gaveeno on Mon Oct 27 at 12:21PM

Great plugin Ryan, thanks!

I made a very minor update to enable me to be able to set the current tab at the action level, not only at the controller level…so I figured I’d share.

In navigation_helper.rb, replace…

1
2
3
4
def current_tab(tab=nil)
    @current_tab = tab unless tab.nil?
    @current_tab ||= controller.controller_name.to_sym
end

With…

1
2
3
4
def current_tab(tab=nil)
    @current_tab = tab unless tab.nil? || @current_tab
    @current_tab ||= controller.controller_name.to_sym
end

Now, I can set the current tab for individual controller actions like so:

1
2
3
4
5
6
7
8
9
10
11
class SiteController < ApplicationController

  def about
    SiteController::current_tab :about_us
  end

  def index
    SiteController::current_tab :home
  end

end

18

Ryan on Mon Oct 27 at 12:33PM

Thanks, I’m glad you find it useful. Regarding your issue with setting the current tab from an action, here’s another option (without modifying any code):

1
2
3
4
5
6
7
8
9
class SiteController < ApplicationController
  def about
    self.class.current_tab :about_us
  end

  def index
    self.class.current_tab :home
  end
end

19

gaveeno on Mon Oct 27 at 01:21PM

Ok sweet, I was trying to figure out a way to do it without modifying the code, making it easier to upgrade if there are future versions of the plugin, but that didn’t occur to me (I’m new to Rails).

Thanks again, this plugin is definitely making my life easier right now.

20

gaveeno on Fri Oct 31 at 11:26AM

Hey Ryan, any idea what I should do to modify this so that I can have subsections (menu items)?

1
2
3
4
5
6
7
8
9
10
11
12
13
<ul class="nav_bar">
    <li class="">
        <a href="/section1">Section 1</a>
        <ul>
            <li>
                <a href = "/section1/subsection1>Subsection 1</a>
            </li>
            <li>
                <a href = "/section1/subsection2>Subsection 2</a>
            </li>
        </ul>
    </li>
</ul>

21

Ryan on Fri Oct 31 at 01:03PM

@gaveeno:

Well, if I were to approach this, here’s how it would look from a usage standpoint:

1
<% navigation [:section_one => [:subsection_one, :subsection_two], :section_two] %>

Considering that, you would have to check the links being passed into the navigation method. While looping, if you come across an array, you could do a recursive call to build the sub-navigation.

1
2
3
4
5
6
7
# to help get the point across
def navigation(links)  
  links.each do |link|
    link_item_for(link) and next unless link.is_a?(Array)
    navigation(link)
  end
end

Of course that’s just pseudo-code, but you get the idea. You’ll probably have to modify the error handling, too, as I’m fairly strict about what gets passed in (considering the ability to add subtitles and such).

Hopefully that gave you some direction. I’ve been contacted a number of times about the sub-navigation issue, but until I have a direct need for it, I probably won’t account for that. However, that’s the beauty of github!

Let me know how it goes.

22

gaveeno on Fri Oct 31 at 01:38PM

Thanks for the pointers Ryan! I’ll give it a shot and let you know if I come up with something workable…though I assure you it won’t be as elegant as the native code.

Have something to say?
Please rewrite the image text Are You Human? Hint: Are You Human? Formatting Tips

or

© 2008 Ryan Heath | Site Management A Ruby on Rails production.

Get in Touch