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.

23

Hans on Mon Feb 16 at 12:21PM

Thanks for this plugin. It is really useful, but I have some problems when using it.

It seems as current_tab does not work for controllers named MenuItemController. (I use the Restful resources ni named maps) However, I added .underscore to the current tab method and then i works fine.

I use role requirement and it would be nice if one could add :authorize => :role in addition to a method. Any ideas about how to implement that feature?

Is it possible to have more than one current item in the same view ? I have two menues and would like to keep the current class in the main menu, when I select an Item in the submenu (implemented as an independent menu in a partial)

24

Ryan on Wed Feb 18 at 10:06AM

@Hans -

Glad you like the plugin.

I’ll have to look into the MenuItemController issue you were having. I haven’t had that problem, but I can see why it would occur. Your underscore solution is more than likely the way to go, since “MenuItem”.downcase => “menuitem” wouldn’t be consistent with RESTful routes. Thanks for catching that.

As far as the :authorize option goes, the idea there is to simply support navigation for a public interface, as well as a “logged in” interface, all in the same helper. It wasn’t designed to handle a sophisticated permissions system.

However, if you wanted to tackle that yourself, you would have to restructure the navigation helper to accept a Hash of options (the latest version accepts a Hash for non-conventional routes, so use that as a guide). And when building out the actual links, you would need to check for a :role option, and if present, only concatenate the link to the list if the current user has that role. Hopefully that makes sense.

And for your final concern, it sounds like you’re wanting nested menus. I’ve done this myself in an application or two, but it never felt generic enough to make its way into the plugin. I added a second controller class method, called navigation_for, that related to the current actions within the controller. You would simply define the display text for each action (defaults to the action name). And in the view, all that is needed was a <%= sub_navigation %> helper call, and the nested current tabs and such were handled automatically. So that’s one approach you could take.

I still have a minor urge to completely rewrite this plugin, and if I ever find the time (and/or motivation) to do so, I may consider some of these recurring interests that people bring up.

If you’re interested, all activity related to this or any new plugins would be on my github account. And thanks for the feedback!

25

Hans on Wed Feb 25 at 02:33PM

Hi again I have now finnished my work with adapting your navigator plugin to my need. Maybe you would be interested in the result? If so, just mail me and I will mail back the code.

As I tried to make my changes as separate from your code as possible, it is not very DRY. That would demand more basic changes of your code. I have not implemented any tests for my additions, but I have have tested it manually and it seems to work at least in my application

I have made the following additions 1. Added translation of item labels in a way that do not interfere with your use of the labels for defining routes. My solution is specific to my I18n helpers, but could easily be made more generic 2. Added the possibility to add an 16*16 icon before the label. If you have an png-image with the name of the label in images/menus folder it will be displayed. 3. Added the possibility to highlight any menu label from any controller and action, using the same solution as you have for defining current tab in controllers. One just declare e.g. highlight_tab :databases and that item wil be highlighted when the corresponding controller is active. That was the way I solved my problem with nested menus in a more generic way 4. Added the possibility to use role requirement as a way to control authorization. I added an option :use with value => role_requirement that use role requirements user_authorized_for? as an authorization method. As this method authorizates the access to a controller and action I just added a call to this method in your plugin. Maybe one can simplify it using your “with” option calling the same method directly, but then I do not know how to transfer the controller and action parameters.

Hans Marmolin Hans.Marmolin@klockholm.se

26

Bharat on Sun Mar 29 at 03:38PM

Hello Hans,
I just tried to send you an email which bounced. I am very interested in your adaptation of Ryan’s code for nested menus and role_requirements plugin since I use both in my solution. Can you please post it somewhere?
Thanks.
Bharat

27

Bharat on Sun Mar 29 at 09:01PM

This is a fantastic plugin. Made my messy code way much cleaner. There is only one thing that I cannot do. That is, keep two menus synchronized. Mind you, I am not talking about directly nested menus, but loosely linked menus. So my top level menu as an Admin tab which is only available for admins, no problems there but then this menu opens up a secondary menu which I have done using a second call to <% = navigation …%> in my application.html.erb as shown below:

1
2
3
4
5
6
<div class='secondary-navigation'>
  <%- if @child_menu && authorize_admin? -%>
    <%= navigation [:crust_types, :toppings] %>
  <%- end -%>
  <div class="clear"></div>
</div>

Now to keep the admin tab highlighted, I put the following code in the AdminsController:

1
2
before_filter :signal_child_menu
current_tab :admin

before filter is calling signal_child_menu which is defined in the ApplicationController as follows:

1
2
3
def signal_child_menu
  @child_menu = true
end

The other two tabs in secondary menu [:crust_types, :toppings] have the same code in corresponding controllers CrustTypesController and ToppingsController respectively as shown below:

1
2
before_filter :signal_child_menu
current_tab :admin

There are two problems that I have:

  1. I cannot keep the Admin tab highlighted along with the selected tab in the secondary menu, i.e., Crust Types or Toppings. It is an either or thing instead of both the top level and secondary menu tabs. I tried putting in multiple calls to current_tab class method in these three controllers, but only one wins.
  2. I am sure that my idea of defining the @child_menu instance variable can be improved upon. It works but is clumsy.

If you have any suggestions to remedy these two issues. I will appreciate it.

Thanks again for sharing this fantastic plugin.

28

Bharat on Sun Mar 29 at 10:24PM

Hello Ryan,
I read your response to Hans. I am interested in the following part:
" … I added a second controller class method, called navigation_for, that related to the current actions within the controller. You would simply define the display text for each action (defaults to the action name). And in the view, all that is needed was a <%= sub_navigation %> helper call, and the nested current tabs and such were handled automatically. So that’s one approach you could take…."

Can you please elaborate on that with respect to your plugin on github? Pseudo code will be fine. I want to do the work and learn.
Thank you.
Bharat

29

Bharat on Mon Mar 30 at 09:31AM

Hello Ryan,
I agree with Hans above. There is a bug in your code which causes the automatic assignment of current class method to fail when you have controller names such as MyPizzasController.rb. The fix for your current_tab class method in navigation.rb file is as follows:

1
2
3
4
def current_tab(tab=nil)        
  self._current_tab = tab if tab
  self._current_tab ||= self.to_s.sub('Controller', '').underscore.downcase.to_sym
end

See the underscore method that was missing in your code? That will fix the problem. It has in my case.

Regards,
Bharat

30

Ryan on Mon Mar 30 at 11:31AM

Bharat -

I can try to elaborate a little more about how I handled the sub-menu, and hopefully you’ll be able to piece it together and fit it to your specific needs.

Basically, this plugin provides navigation at the controller level. The current tab is selected based on the current controller being accessed. Well, my view of the sub-menu is then at the action level.

To start things off, in my case, I always had a sub-menu for every main tab, so in my layout I chose the area where the sub-menu was to go, and used:

1
<%= yield :sub_navigation %>

I’m assuming you’re familiar with content_for, here. Having that in place allows me to construct helper methods that will build the links and then use content_for to fill in the sub-menu. This let’s me call the helper anywhere, without requiring explicit placement within the layout/view.

1. The navigation_for class method

I added a class method (the same place the current_tab method is defined) to all controllers that allows me to setup sub-menu’s for that controller’s actions. So at the top of a controller, I might have this:

1
navigation_for [:index, 'All Tasks', :new, 'New Task']

Essentially, all that method does is setup two arrays: one for the actions that are to be a part of the menu, and one for the text that is to be displayed for each of the actions. It then calls a before_filter to setup those arrays (via instance variables) to the view. The method looks something like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# used to specify sub-navigation per controller
#
# Example:
# class DashboardController < ApplicationController
# navigation_for [:index, 'Stats', :welcome, 'Welcome']
# end
def navigation_for(items)
  before_filter do |klass|
    actions, texts = [], []
    
    items.each_with_index do |item, index|
      actions << item if item.is_a?(Symbol) && index.even?
      texts   << item if item.is_a?(String) && index.odd?
    end
    
    klass.send(:setup_sub_navigation, [actions, texts])
  end
end

The setup_sub_navigation helper just loads the actions and texts into instance variables so the view can use them.

2. The sub_navigation helper

Once things are setup, all I have to do then is call <%= sub_navigation %> as needed. And that method looks very similar to the <%= navigation ... %> helper, except instead of checking the controller name or current tab, it checks for the action name.

Also, if you take this approach, don’t forget to call content_for at the end of the sub_navigation helper. Something like this:

1
2
3
content_for :sub_navigation do
  content_tag :ul, list_items, :class => 'sub_navigation'
end and return nil

Where the list_items are the constructed elements that you would have built above that line using the instance variables from setup_sub_navigation.

And that’s it. There are seriously a ton of different ways to set this up, but for my situation, I wanted this to be defined in the controller, so it became immediately clear which actions were a part of the sub-menu. Another nice side-effect is that it doesn’t bother the main controller-level tabs at all, so you can have two “tabs” highlighted at once.

Let me know if any of that is confusing and I can try to explain it a little more/better.

Hope that helps!

31

Bharat on Mon Mar 30 at 12:16PM

Thanks a lot Ryan. I need to grok what you have written above and try to implement it before I can ask any intelligent questions.
It looks like both the Ryans (Bates and Heath) are taking me to school. I love it. You both have a very practical and down-to-earth coding approach which intuitively appeals to me. For that matter, Tim Harper’s roles_requirement plugin rocks too.
Regards,
Bharat

32

Bharat on Thu Apr 02 at 05:44PM

Hello Ryan,
Making excellent progress. I am trying to call the current_tab method conditionally in my users_controller as shown below:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class UsersController < ApplicationController
  
  before_filter :login_required
  #current_tab(self.authorize_admin? ? :clients : :basic_data)
  current_tab :clients
  
  def index  
  end
  
  def edit
    self.class.current_tab :basic_data unless authorize_admin?
  end
  ...
end

This works. But when I try the uncommented version of conditional setting of current_tab, I get an error that authorize_admin? method is not defined. This method is defined in authentication.rb file in the lib directory in a module which is then mixed into the ApplicationController class. So I guess what I am asking is can an instance method be called from the current_tab method?

Bharat

33

Bharat on Thu Apr 02 at 05:45PM

apologies. forgot to format the code.

34

Ryan on Thu Apr 02 at 07:39PM

Bharat -

I fixed the formatting :-)

And the answer to your question is no, you cannot. The current_tab method is a class/static method, and therefore has no idea (nor should it) about the “instance” of whatever controller you’re dealing with.

Maybe you could do something like this:

1
2
3
4
5
6
7
8
9
class UsersController < ApplicationController
  before_filter :set_current_tab
  ...
  
protected
  def set_current_tab
    self.class.current_tab(authorize_admin? ? :clients : :basic_data)
  end
end

When you pass the method reference to a before filter, it executes at the instance level before each action, which means you would then have access to your authorize_admin? instance method.

Let me know if that solves your problem.

35

Peter on Fri Apr 10 at 07:28AM

Hi I’m trying to use your plugin but I keep on getting the below error. I was wondering if you could let me know why I would be getting this error. I’m new to ruby and rails.

undefined method `navigation’ for #

my application.rhtml.erb

1
2
3
4
5
6
7
8
9
10
11
12
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
        "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
  <head>
    <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
    <title><%= 'Pre-Heat - ' + @pageTitle || 'Pre-Heat' %></title>
  <body>
  	<%= navigation [:home, :about_us, :our_services] %>
  	
  	<%= yield %>
  </body>
</html>

36

Ryan on Fri Apr 10 at 09:05AM

Peter -

I’m not sure why you’re getting that error. Have you restarted your app after plugin installation? If you’re using passenger, do: $> touch tmp/restart.txt. Otherwise just ctrl+c and script/server again.

The pugin mixes into ActionView::Base so you shouldn’t have any issues calling the navigation helper once it’s installed and your app has been restarted.

The only other thing in question: is your layout really named application.rhtml.erb? It could just be a typo, but proper syntax should be application.html.erb. In prior versions of Rails, the templates had a .rhtml extension (ruby html), but now things are in the form of name.type.renderer. Maybe .rhtml.erb still works, I don’t know. But it should be application.html.erb.

37

Peter on Fri Apr 10 at 09:27AM

Hi Ryan, thanks for replying

Yeah that was a typo, sorry about that.

I’ve tried removing the plugin, restarting the server and then installing the plugin and again restarting the server. I’m still geting the same error. Is there anything I need to do when I install the plugin? All I’m doing is adding <%= navigation [:home, :about_us, :our_services] %> in my view file (application.html.erb). Do I need to include anything in my controllers?

38

Peter on Fri Apr 10 at 09:40AM

Hi Ryan,

I’ve decided to start from scratch…… I’m now getting a different error and I think its to do with my routes.rb. Below is the error I’m getting.

undefined method `home_path’

here is my routes.rb

1
2
3
4
5
6
7
8
9
map.with_options(:controller => "about_us" ) do |about_us|
  about_us.connect "aboutus", :action => "index"
  #store_map.connect "/titles/buy/:id" , :action => "add_to_cart"
  #store_map.connect "/cart" , :action => "display_cart"
end
 	
map.with_options(:controller => "our_services" ) do |our_services|
  our_services.connect "ourservices", :action => "index"
end	

Anything you can spot I’m doing wrong?

39

Peter on Fri Apr 10 at 09:42AM

oh missed one!

1
2
3
map.with_options(:controller => 'home') do |home|
  home.connect '', :action => 'index'
end

40

Chris Scharf on Fri Apr 10 at 10:26AM

Peter,

The missing home_path is because you haven’t defined a named route. To do that, you replace .connect with the name (“home”) in this case.

Also, map.connect '', ... is better represented with map.root ....

So, your “home” route should be like this:

1
map.root :controller => 'home'

Then, use :root instead of :home in the navigation helper call:

1
<%= navigation [:root, :about_us, :our_services] %>

Though you could define a “home” named route if you wanted:

1
map.home '', :controller => 'home'

41

Ryan on Fri Apr 10 at 10:26AM

Peter -

Ah, yeah, you have to setup your routes first. Have you heard of named routes? You should definitely be using them (instead of map.connect for instance). This plugin assumes you’re using named routes, which is why you’re having issues. Here’s a brief run down of how they work, but you should look into it in more detail here.

I’ll use your home controller as the guinea pig. You essentially have:

1
map.connect '', :controller => 'home', :action => 'index'

You could/should do:

1
map.home '', :controller => 'home', :action => 'index'

See how I’ve used ‘home’ instead of ‘connect’ as the method called on the map object? You can type anything you want there. Rails will automatically generate some helpers for you. In this particular case, the ones of interest are:

  • home_path
  • home_url

The home_path helper just returns the absolute path (‘/’ in this case), where home_url will return the full path (‘http://domain.com/’ for example).

In your case, you will want something like this for the plugin to work properly:

1
2
3
map.home '/', :controller => 'home', :action => 'index'
map.about_us '/about', :controller => 'about', :action => 'index'
map.our_services '/services', :controller => 'services', :action => 'index'

Just a tip: I’d refrain from using with_options until you have multiple routes per controller. It’s overkill to use it for a single route, as the point is just to remove the :controller => 'whatever' duplication.

Just so you know why that is, the symbols that you pass to navigation in your layout will get converted to "#{name}_path" pretty much. That’s why you have to have routes that map to the names passed to the navigation helper (another benefit of named routes).

Also know that you can (in the latest version) pass in custom routes, like so:

1
<%= navigation [{:home => some_other_path}, :about_us, :our_services] %>

Also, the helpers will be available to controllers and views, so now in your controllers you could have:

1
redirect_to home_path

instead of:

1
redirect_to :controller => 'home', :action => 'index'

The benefits are obvious, but just imagine having to change those redirects? With named routes you just change them in the routes file instead of all over the place. Another useful routing helper is map.resources. I’ll let you look into that on your own, but it deals with RESTful routing, a best-practice in the Rails world.

Let me know if you’re confused or if you’re still having problems with the plugin. Hope that helps.

42

Peter on Fri Apr 10 at 11:31AM

Chris & Ryan,

Thank you so much for your quick replys. It all works now. I’ll read up on routing seems like it can be quite powerful. Just got to work out how to do sub navigation now…

43

Peter on Fri Apr 10 at 01:59PM

one more thing.

I’ve just noticed the “current” class is not showing on all pages except the home page. Below is my routes.rb now. Any idea why it might be doing this?

1
2
3
4
5
map.home '/', :controller => 'home', :action => 'index'
map.about_us '/aboutus', :controller => 'about_us', :action => 'index'
map.our_services '/ourservices', :controller => 'our_services', :action => 'index'
map.connect ':controller/:action/:id'
map.connect ':controller/:action/:id.:format'

44

Ryan on Fri Apr 10 at 04:00PM

Peter -

Try changing this line, which looks like this:

1
2
# lib/navigation.rb
self._current_tab ||= self.to_s.sub('Controller', '').downcase.to_sym

to this:

1
2
# lib/navigation.rb
self._current_tab ||= self.to_s.sub('Controller', '').underscore.to_sym

I’m aware of the needed fix, but have yet to do it. It prevents controllers having multiple case (i.e. AboutUsController) from matching properly.

Let me know if that fixed your issue.

45

Peter on Fri Apr 10 at 05:19PM

Hi Ryan,

No that did not fix the problem. If I use your “current_tab” within the controller it works, i.e. current_tab :about_us.

Let me know if you want me to try something else for you?

46

Ryan on Fri Apr 10 at 09:28PM

Peter -

I’m pretty sure that fix will work for you. But you should know, when you modify anything in lib/ or vendor/plugins, you need to restart your server for those changes to take effect (meaning you can’t just change it and then refresh the page). Try that, then let me know if you’re still having trouble. But again, I’m almost positive that will do the trick.

47

Peter on Sat Apr 11 at 07:23PM

Hi Ryan,

Yeah it works now. I’ll remember to do that in the future.

Thanks..

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

or

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

Get in Touch