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:
- whenever you see
navigation(...), think of<%= navigation(...) -%>as that method is a helper used in a view - 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.
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.
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:
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:
The section/subtitle association is a lot more obvious that way. But there’s a problem with that: a
Hashis an unordered container. So then I thought, no biggy, if aHashis 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:
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:
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 ?
I have added the following lines in my routes.rb file :
Any idea ?
11
Ryan on Wed Mar 05 at 08:04AM
Yeah, minor problem with your routes. You need:
From that, Rails will build helpers for you based on the map.[whatever] part. So for
map.homeyou’ll gethome_path,home_url, etc. In your case, though, you’re gettinghome_path_pathbecause 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:
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 defaultindexaction of theusers_controller), and usecurrent_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
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…
With…
Now, I can set the current tab for individual controller actions like so:
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):
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)?
<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:
Considering that, you would have to check the links being passed into the
navigationmethod. While looping, if you come across an array, you could do a recursive call to build the sub-navigation.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.