November 1, 2016 · rails erb

Nested Layouts in Rails

Let’s take a look at Rails layouts and how to make them more manageable. By default, there is a main layouts/application.html.erb file with a yield statement. The views then render their content into that yield.

<%# app/views/layouts/application.html.erb %>
<!DOCTYPE html>
<html>
  <head>
    <title>Online Store</title>
    <%= stylesheet_link_tag "application" %>
  </head>
  <body>
    <%= render "header" %>
    <%= yield %>
    <%= render "footer" %>
  </body>
</html>

This works great initially, but can quickly get out of hand when you need variations of the layout. You either end up with messy conditional logic in the layout or lots of repetition and partials in the views.

We will add some complexity to the layout above with a new requirement: a sidebar should only be displayed on product pages.

Add sidebar directly to views

There are only two product views: index and show. We can meet the layout requirement by adding the sidebar directly to each product view.

<%# app/views/products/index.html.erb %>
<div class="eight columns">
  <%= render @products %>
</div>

<div class="four columns">
  <%= render "sidebar" %>
</div>
<%# app/views/products/show.html.erb %>
<div class="eight columns">
  <%= render @product %>
</div>

<div class="four columns">
  <%= render "sidebar" %>
</div>

Not awful, but five out of six lines are duplicate, so yeah, not great. In this scenario, any changes to the sidebar layout require altering all the product views.

Add a separate sidebar layout

Let’s remove the duplication and instead add a sidebar layout.

<%# app/views/layouts/sidebar.html.erb %>
<!DOCTYPE html>
<html>
  <head>
    <title>Example Website</title>
    <%= stylesheet_link_tag "application" %>
  </head>
  <body>
    <%= render "header" %>

    <div class="eight columns">
      <%= yield %>
    </div>

    <div class="four columns">
      <%= render "sidebar" %>
    </div>  

    <%= render "footer" %>
  </body>
</html>

We specify the layout in the ProductsController. This causes all views rendered by the controller to use the sidebar layout.

# app/controllers/products_controller.rb
class ProductsController < ApplicationController
  layout "sidebar"

  def index
    @products = Product.all
  end

  def show
    @product = Product.find(params[:id])
  end
end

We can remove the duplicate code in the views.

<%# app/views/products/index.html.erb %>
<%= render @products %>
<%# app/views/products/show.html.erb %>
<%= render @product %>

Ahhh. So much nicer. And, as an added benefit, we are able to reuse the sidebar layout in other controllers with a single line of code. However, we now have a different duplication nightmare. Any adjustments to the application layout must also be made to the sidebar layout.

Convert sidebar layout to a nested layout

This is where nested layouts are really useful. We can eliminate duplication between layouts by only providing the specialized parts.

Add a helper method called inside_layout.

# app/helpers/application_helper.erb
module ApplicationHelper
  def inside_layout(layout = "application", &block)
    render inline: capture(&block), layout: "layouts/#{layout}"
  end
end

This method takes a layout and a block of HTML/ERB. The capture method converts the provided block to a string and passes it along with the template to render. This results in the provided content being wrapped in the specified layout and rendered.

The sidebar layout can be simplified to only include the parts that are different.

<%# app/views/layouts/sidebar.html.erb %>
<%= inside_layout "application" do %>
  <div class="eight columns">
    <%= yield %>
  </div>

  <div class="four columns">
    <%= render "sidebar" %>
  </div>  
<% end %>

So much better. Instead of duplicating the application layout we are extending it. The application layout becomes the outermost shell while the nested layouts become specialized and reusable.