views:

1240

answers:

9

I have a Ruby/Rails app that has two or three main "sections". When a user visits that section, I wish to display some sub-navigation. All three sections use the same layout, so I can't "hard code" the navigation into the layout.

I can think of a few different methods to do this. I guess in order to help people vote I'll put them as answers.

Any other ideas? Or what do you vote for?

+5  A: 
  1. Partial render. This is very similar to the helper method except perhaps the layout would have some if statements, or pass that off to a helper...
Matt Rogish
as in render :partial => 'navigation', right?
webmat
yep, something like render :partial => 'section1_subnav'
Matt Rogish
+1  A: 

You could use something like the navigation plugin at http://rpheath.com/posts/309-rails-plugin-navigation-helper

It doesn't do sub-section navigation out of the box, but with a little tweaking you could probably set it up to do something similar.

madlep
+1  A: 

I suggest you use partials. There are a few ways you can go about it. When I create partials that are a bit picky in that they need specific variables, I also create a helper method for it.

module RenderHelper
  #options: a nested array of menu names and their corresponding url
  def render_submenu(menu_items=[[]])
    render :partial => 'shared/submenu', :locals => {:menu_items => menu_items}
  end
end

Now the partial has a local variable named menu_items over which you can iterate to create your submenu. Note that I suggest a nested array instead of a hash because a hash's order is unpredictable.

Note that the logic deciding what items should be displayed in the menu could also be inside render_submenu if that makes more sense to you.

webmat
+6  A: 

As for the content of your submenus, you can go at it in a declarative manner in each controller.

class PostsController < ApplicationController
#...
protected
  helper_method :menu_items
  def menu_items
    [
      ['Submenu 1', url_for(me)],
      ['Submenu 2', url_for(you)]
    ]
  end
end

Now whenever you call menu_items from a view, you'll have the right list to iterate over for the specific controller.

This strikes me as a cleaner solution than putting this logic inside view templates.

Note that you may also want to declare a default (empty?) menu_items inside ApplicationController as well.

webmat
Wouldn't it be better to have the text for the submenu options in the View rather than the Controller?
Olly
This is the wortest approach you could chose. As above comment pointed out, it breaks MVC completely.
maurycy
+2  A: 

Warning: Advanced Tricks ahead!

Render them all. Hide the ones that you don't need using CSS/Javascript, which can be trivially initialized in any number of ways. (Javascript can read the URL used, query parameters, something in a cookie, etc etc.) This has the advantage of potentially playing much better with your cache (why cache three views and then have to expire them all simultaneously when you can cache one?), and can be used to present a better user experience.

For example, let's pretend you have a common tab bar interface with sub navigation. If you render the content of all three tabs (i.e. its written in the HTML) and hide two of them, switching between two tabs is trivial Javascript and doesn't even hit your server. Big win! No latency for the user. No server load for you.

Want another big win? You can use a variation on this technique to cheat on pages which might but 99% common across users but still contain user state. For example, you might have a front page of a site which is relatively common across all users but say "Hiya Bob" when they're logged in. Put the non-common part ("Hiya, Bob") in a cookie. Have that part of the page be read in via Javascript reading the cookie. Cache the entire page for all users regardless of login status in page caching. This is literally capable of slicing 70% of the accesses off from the entire Rails stack on some sites.

Who cares if Rails can scale or not when your site is really Nginx serving static assets with new HTML pages occasionally getting delivered by some Ruby running on every thousandth access or so ;)

Patrick McKenzie
Upvotes for originallity of thought. :-)
maurycy
+1  A: 

I asked pretty much the same question myself: Need advice: Structure of Rails views for submenus? The best solution was probably to use partials.

Christoph Schiessl
+1  A: 

There is another possible way to do this: Nested Layouts

i don't remember where i found this code so apologies to the original author.

create a file called nested_layouts.rb in your lib folder and include the following code:

module NestedLayouts
  def render(options = nil, &block)
    if options
      if options[:layout].is_a?(Array)
        layouts = options.delete(:layout)
        options[:layout] = layouts.pop
        inner_layout = layouts.shift
        options[:text] = layouts.inject(render_to_string(options.merge({:layout=>inner_layout}))) do |output,layout|
          render_to_string(options.merge({:text => output, :layout => layout}))
        end
      end
    end
    super
  end
end

then, create your various layouts in the layouts folder, (for example 'admin.rhtml' and 'application.rhtml').

Now in your controllers add this just inside the class:

include NestedLayouts

And finally at the end of your actions do this:

def show
  ...
  render :layout => ['admin','application']
end

the order of the layouts in the array is important. The admin layout will be rendered inside the application layout wherever the 'yeild' is.

this method can work really well depending on the design of the site and how the various elements are organized. for instance one of the included layouts could just contain a series of divs that contain the content that needs to be shown for a particular action, and the CSS on a higher layout could control where they are positioned.

Jason Miesionczek
+3  A: 

You can easily do this using partials, assuming each section has it's own controller.

Let's say you have three sections called Posts, Users and Admin, each with it's own controller: PostsController, UsersController and AdminController.

In each corresponding views directory, you declare a _subnav.html.erb partial:

/app/views/users/_subnav.html.erb
/app/views/posts/_subnav.html.erb
/app/views/admin/_subnav.html.erb

In each of these subnav partials you declare the options specific to that section, so /users/_subnav.html.erb might contain:

<ul id="subnav">
  <li><%= link_to 'All Users', users_path %></li>
  <li><%= link_to 'New User', new_user_path %></li>
</ul>

Whilst /posts/_subnav.html.erb might contain:

<ul id="subnav">
  <li><%= link_to 'All Posts', posts_path %></li>
  <li><%= link_to 'New Post', new_post_path %></li>
</ul>

Finally, once you've done this, you just need to include the subnav partial in the layout:

<div id="header">...</div>    
<%= render :partial => "subnav" %>
<div id="content"><%= yield %></div>
<div id="footer">...</div>
Olly
A: 

There are few approaches to this problem.

You might want to use different layouts for each section.

You might want to use a partial included by all views in a given directory.

You might want to use content_for that is filled by either a view or a partial, and called in the global layout, if you have one.

Personally I believe that you should avoid more abstraction in this case.

maurycy