website stat

Nested layouts in Ruby on Rails

Sometimes it's quite useful to have nested layouts. For instance, suppose you have a main layout that should be applied crosswide to your website. This way, what changes is only a tiny part usually defined as @content_for_layout.

Nonetheless, you may need to customize even further that content placer. Eventually with a new layout, but always inheriting the main one.

This feature is not native to Rails. Fortunately I came accross a suggestion on a wiki on how to do accomplish such. Since wikis are somewhat momentary, I hereby copy it to here with the due credits (Maxim Kulkin, inspired by ASP.NET 2.0 Master Pages implementation).

Start by creating a file like nested_layouts.rb and put anywhere you like (usually inside /lib or /conf). Put the following as the content:

For Rails < 1.2

RUBY:
  1. module ActionView
  2.   module Helpers
  3.     module NestedLayoutsHelper
  4.       def inside_layout(layout, &block)
  5.         layout = layout.include?('/') ? layout : "layouts/#{layout}"
  6.  
  7.         concat(@template.render_file(layout, true, '@content_for_layout' => capture(&block)), block.binding)
  8.       end
  9.     end
  10.   end
  11. end
  12.  
  13. ActionView::Base.class_eval do
  14.   include ActionView::Helpers::NestedLayoutsHelper
  15. end

For Rails > 1.2 (Hands down to Matthew Bass for the tip).

RUBY:
  1. module ActionView
  2.   module Helpers
  3.     module NestedLayoutsHelper
  4.       def inside_layout(layout, &block)
  5.         layout = layout.include?('/') ? layout : "layouts/#{layout}"
  6.  
  7.         @template.instance_variable_set("@content_for_layout", capture(&block))
  8.         concat(@template.render(:file => layout, :user_full_path => true), block.binding)
  9.       end
  10.     end
  11.   end
  12. end
  13.  
  14. ActionView::Base.class_eval do
  15.   include ActionView::Helpers::NestedLayoutsHelper
  16. end

Which does nothing until we actually call it. Put the following line inside environment.rb

RUBY:
  1. require "#{RAILS_ROOT}/config/nested_layout.rb"

and then restart the webserver.

Now you're ready to make full usage of rails nested layouts.

In controller write

RUBY:
  1. class FooController <ApplicationController
  2. layout 'inner'
  3. end

Then inside your ‘layouts/inner.rhtml’ do

RUBY:
  1. <% inside_layout 'outer' do %>
  2. Inner layout header
  3. <%= @content_for_layout %>
  4. Inner layout footer
  5. <% end %>

The outer layout can also nest itself inside a higher level layout.


10 Responses to “Nested layouts in Ruby on Rails”

  1. Sean
    Published at August 27th, 2006 at 4:21 pm

    Hi,

    This looks like a very good technique. I have tried to use it on my site but I get the following error:
    ActionView::TemplateError (undefined method `inside_layout’ for #:0×3405380>) on line #1 of app/views/layouts/store.rhtml:

    That’s weird because I required the file in /lib and the require doesn’t throw any errors, so the method must be there, right?

    Thanks,
    Sean

  2. Sean
    Published at August 27th, 2006 at 4:58 pm

    Okay, I discovered that this is required in the controller:
    helper “lib/NestedLayouts”

    Now I just need to find out how to access controller variables. e.g. in my controller I have

    def index
    @releases = Release.find :all
    end

    And I get the following error:

    ActionView::TemplateError (You have a nil object when you didn’t expect it!
    You might have expected an instance of Array.
    The error occurred while evaluating nil.each) on line #3 of app/views/store/index.rhtml:
    1:
    2: Recent Items
    3:

    Obviously the technique is no good if you can’t acess variable from the controller methods… oh well.

  3. Sean
    Published at August 28th, 2006 at 1:24 am

    After reviewing the code more carefully I discovered that the problem was simply that I had omiited the last 3 lines that go in the /lib file. Sorry.

    On another note, I discovered that the technique is not compatible with Cod Hale’s Content-only caching for Rails plugin: http://blog.codahale.com/2006/04/10/content-only-caching-for-rails/

    It seems the plugin cache’s content_for_layout and then pipes that to the layout, but since the cached version has already had the inner layout added in by this technique, the inner layouts appear twice. I wonder if there’s any way around that?

    Thanks,
    Sean

  4. mlopes
    Published at August 28th, 2006 at 3:30 pm

    Hi Sean, sorry for taking so much for answering. I’ve been off.

    I’m deeply interested in the content caching you’ve linked to but I must confess I haven’t tried it yet with the inner layout technique. I’ll do it as soon as possible since I have one major update going on one of my websites.

    Please let me know if you did any breakthrough on this issue. I’ll keep you updated through here and through your email.

    Cheers,

    Mário

  5. Jeremy
    Published at August 30th, 2006 at 4:48 pm

    Mário

    Just to let you know, you say to call the file ‘nested_layouts.rb’, but the require specifies ‘nested_layout.rb’ (with only one ’s’).

  6. Dallas
    Published at October 13th, 2006 at 10:16 am

    Origially, I just wanted to say that I had the same trip-up as Jeremy.. but then I pressed submit without filling out my email and was taken to a very un-friendly validation error page with no links.

    After clicking back.. my message was lost.

    Please consider a more friendly approach to your comment form validation.

  7. Rick Martinez
    Published at February 15th, 2007 at 10:18 pm

    Have there been any updates to this bit of code? It seems that in Rails 1.2, whenever you return to a page that has been rendered before with this code, the content shows up as blank and you have to restart the web server in order for it to work again. Anyone else experiencing this?

  8. mlopes
    Published at February 16th, 2007 at 3:34 pm

    Rick,

    I’m experiencing the very same you described. I haven’t yet figured a solution and in the meanwhile I’m reverting to Rails 1.1.6.

  9. Matthew Bass
    Published at February 20th, 2007 at 4:54 am

    Rick and mlopes, I’m experiencing the same problem as well on Rails 1.2.2. Haven’t found a workaround yet. Has anyone else managed to fix this problem?

  10. Matthew Bass
    Published at February 20th, 2007 at 2:52 pm

    I managed to solve this problem by using #set_instance_var to set @content_for_layout in the template instead of passing it in as a local. Something like this:

    @template.instance_variable_set(”@content_for_layout”, content_for_layout)
    concat(@template.render(:file => layout, :user_full_path => true), block.binding)