26 Oct, 2009

Published at 04:15PM

Tagged with github, plugin, programming, rails, and tips

This post has 6 comments

New Rails Navigation plugin

I mentioned before how I was getting the urge to rewrite my navigation_helper plugin, and now I finally have. But really, it takes a totally different approach, so it’s almost Just Another Navigation Plugin (JANP) and not a rewrite. Either way, you can find it on GitHub.

I don’t want to duplicate the README, but I’ll briefly touch on a few things.

Installation

Just to be explicit: navigation plugin on GitHub. Install it like so:

1
script/plugin install git://github.com/rpheath/navigation.git

Now onto the usage.

Menu Definition

This plugin requires pre-defined menus before they can be rendered in your views. This is mainly because I like writing straight-up Ruby code more than I like writing ERB code. So, here’s an example menu definition:

1
2
3
4
5
6
RPH::Navigation::Builder.config do |navigation|
  navigation.define :authenticated do |menu|
    menu.item HomeController, :text => 'Welcome'
    menu.item ContactController, :path => :contact_us_path
  end
end

The defaults are as you might expect. If no :text or :path options are present, the text would be the controller’s name, titleized, and the path would be the route that matches the controller (for HomeController expect home_path).

Also, you can define as many menus as your application needs using the yielded navigation object. And once you have your menus defined, you can render them via the key used in the definition:

1
<%= navigation :authenticated %>

Rule-based Menus and Items

You can pass an :if option to the menu itself and each menu item independently if you have some custom rules that need to be applied before rendering either the menu or a particular item. Here’s how it works:

1
2
3
4
5
6
7
RPH::Navigation::Builder.config do |navigation|
  navigation.define :authenticated, :if => Proc.new { |view| view.logged_in? } do |menu|
    menu.item HomeController, :text => 'Welcome'
    menu.item ContactController, :path => :contact_us_path
    menu.item AdminController, :if => Proc.new { |view| view.logged_in_as_admin? }
  end
end

Notice the :if option after the :authenticated key. Based on the true/false nature of that Proc, that will either render or not render the entire menu. Now, if you look down to the last menu.item, you can see another :if condition used there. That will simply render or not render that specific menu item.

The Proc will be automatically handed the view instance, so any method that ActionView::Base is aware of can be called here. And if you need the controller, just use view.controller.

There are several other things worth checking out, like support for nested menus (just pass a block to the menu.item call), separate action-level menus, and so on. Again, the README has all of the details.

This is the one area in Rails that I don’t like to think much about once it’s done, so I’m trying to come up with a solution that works for every application, despite the number of edge cases. So far, this is probably the closest I’ve come to that realization. I’m currently using it on 2 production applications and things are going just fine so far.

Questions/comments welcome, and as always, feel free to fork and improve!

Comments

Jason R. Thursday, 29 Oct, 2009 Posted at 02:07AM

Hi Ryan, after a long while of searching for different menu navigation options I found yours here. Looks like its just what I want too! Very nice. I’m playing around with it now and I’m wondering if there is a way to wrap <span> tags inside of the <a> tags. Like this:

1
2
3
4
<ul>
   <li class="active"><a href="/"><span>Home</span></a></li>
   <li><a href="races"><span>Contact</span></a></li>
</ul>

The reason I want the spans is for design purposes. If you want rounded corners on horizontal tabs then I think spans inside the anchors are the best way to go. I new to ruby so I’m struggling with the mod. Any help is greatly appreciated. Thanks!

Ryan Thursday, 29 Oct, 2009 Posted at 09:17AM

Jason R -

If I need to style the link nested in an <li> tag, I usually style the link itself, starting with display:block (so vertical padding works properly). However, I realize each person has their own process. So, there are a couple of options…

1) Setting the :text option to include a span tag

1
2
3
4
5
6
RPH::Navigation::Builder.config do |navigation|
  navigation.define :primary do |menu|
    menu.item HomeController, :text => '<span>Home</span>'
    menu.item RacesController, :text => '<span>Contact</span>'
  end
end

That will work without requiring you to modify the code. However, if you definitely want this behavior permanently, you only have to modify one line.

2) Update the code to permanently include a span tag

In lib/navigation/navigator.rb, around line 80, you should see a line like this:

1
items << self.view.content_tag(:li, self.view.link_to(text, path) + subnav, attrs)

All you have to do is wrap the text with a span tag, so change it to this:

1
items << self.view.content_tag(:li, self.view.link_to(self.view.content_tag(:span, text), path) + subnav, attrs)

You can literally cut-n-paste if you want. Hope that helps and let me know if you’re still stuck. Also, since both cases happen at initialization, don’t forget to restart your server after making the change.

Jason R Friday, 30 Oct, 2009 Posted at 02:15AM

Thanks for the quick response and help Ryan. I figured out option #2 late last night and it works great. But option #1 might be better for me because I don’t really want to make changes to your plugin since I’d have to redo them each time I get updates for your plugin :). So far it’s working great.

I did end up commenting out the validation that makes sure the controller exists. I wanted to see my navigation on a page even thought I didn’t have all the conrtollers.

I made a modifcation that I recommend be put in your plugin. It allows you the specify the class name for the current menu item. I’ve seen ‘current’ and ‘active’ used.

Ryan Friday, 30 Oct, 2009 Posted at 07:34PM

Jason R -

Cool, glad things are working out. Also, just so you know, you don’t have to pass the controller object to the menu items. You can pass a symbol or a string if you want.

1
menu.item :home, :path => :root_path, :text => 'Welcome'

Also, if you want to just check/style tabs before implementing any code/routes, just keep using the same menu item with different text.

1
2
3
menu.item HomeController, :path => :root_path, :text => 'Welcome'
menu.item HomeController, :path => :root_path, :text => 'About Us'
menu.item HomeController, :path => :root_path, :text => 'Contact Us'

Sure, the tabs will all link to the same place, but that might be a better solution than commenting out validations and such. Just an idea.

And yes, I’ve considered making it a per-menu option for the CSS class of the “current” or “active” menu item(s). I have such a strong habit of “current” but that doesn’t mean everyone does :-)

Christoph Pilka Monday, 08 Mar, 2010 Posted at 04:14AM

Hi Ryan,

very cool approach for multi-level navigation! Do you have some hints covering i18n of the nav labels? The menus are defined statically in conf file so I’m not sure how to use Rails i18n for my purpose.

Cheerio, Chris

Ryan Monday, 08 Mar, 2010 Posted at 07:12PM

Christoph Pilka -

You could probably do something like:

1
menu.item WelcomeController, :path => :root_path, :text => :greeting_text

Then, in lib/navigation/navigator.rb (around line 105), you can see how the text is being created. This line in particular:

1
text = opts.delete(:text) || item.to_s.titleize

You could probably try changing it to something like this:

1
text = opts[:text].is_a?(Symbol) ? I18n.t(opts.delete(:text)) : item.to_s.titleize

Of course this would assume you had your language.yml files setup to handle the :greeting_text key. Also, this is untested, but just might be as simple as that.

Let me know if you try this and run into any hiccups, I’d be happy to help. Internationalization is probably something I should add support for, anyway.

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