Modular Page Assembly in Rails (Part 2)

In Part 1, I explained how you can develop clean, DRY and encapsulated MVC code that allows for completely modular page assembly in Ruby on Rails.

In this follow up post, I explain how you can use a combination of layouts and content_for to apply title bars and consistent styles to your page components (or modules or portlets, or whatever you want to call them).

The code here is easy to follow and it pretty much speaks for itself. It consists of two layouts (which I called aggregate and snippet), a sample controller and two sample views. Let’s start with the controller for one page in your site that, say, aggregates a couple of modular page components together to show a nice view of company information.

Controller code (app/controllers/company_controller.rb):

def index
  render :action => 'index', :layout => 'aggregate'  
end

This controller simply delegates the rendering of the page to the index.html.erb view and tells Rails to use a layout called aggregate.

Now let’s inspect the view.

View code (app/views/company/index.html.erb):

<% content_for :left do %>
 <%= embed_action :controller => 'company', :action => 'company_list' %>
 <%= embed_action :controller => 'feed', :action => 'feed' %>
<% end %>

<% content_for :center do %>
 <%= embed_action :controller => 'company', :action => 'detail' %>
<% end %>

<% content_for :right do %>
 <%= embed_action :controller => 'home', :action => 'sponsors' %>
<% end %>

This view defines three content regions, with the end goal being to create a page with three columns of “portlets.” The left column contains two portlets: a list of all companies (company_list) and a news feed (feed). The center column contains a company detail portlet and the right column contains a portlet with information about sponsors. (Note that the portlets come from three different functional areas of the site, so they’re decomp’d appropriately into three different controllers.)

Now, let’s take a look at some layout magic.

Here’s the aggregate layout (app/views/layouts/aggregate.html.erb):

<table class="main" cellspacing="0" cellpadding="0">
  <tbody>
    <tr>
      <td id="column-left" class="column" valign="top">
        <div id="region-left" class="region"><%= yield :left %></div>
      </td>
      <td id="column-center" class="column" valign="top">
        <div id="region-center" class="region"><%= yield :center %></div>    
      </td>
      <td id="column-right" class="column" valign="top">
        <div id="region-right" class="region"><%= yield :right %></div>
      </td>
    </tr>
  </tbody>
</table>

I chose a table (yes, I still use tables) with three divs in it, one for each region of modules, but you could use pure divs with floating layouts or any other approach.

The three content regions, left, center and right, match up with the three content sections defined in the index view above using content_for. In case this isn’t obvious, when the layout encounters a page-level definition of a content region in the view, it renders it. If there is no definition for a particular region, the containing column will just collapse on itself, which is the behavior we want.

This is a slight digression, but note how I used CSS classes to identify the columns and regions in a general way (using classes) and a specific way (using ids). This allows me to style the whole module-carrying region with CSS using table.main as my selector, all the columns using table.main td.column as my selector or all the regions using table.main td.column div.region as my selector. I can also pick and choose different specifc areas (e.g. table.main td.column#column-right) and define their style attributes using CSS. As you’ll see in a minute, I can write CSS selectors to say if module A is in the left column, apply style X but if module A is in the center or right column, apply style Y. Pretty cool.

Now, let’s explore the module layout. (Note that I’ve been calling page snippets portlets, modules or components, pretty much interchangeably. I think this illustrates that it doesn’t make a difference what we call ’em — e.g. portlets vs. gadgets — the concept is fundamentally clear and fundamentally the same.)

Module layout (app/views/layouts/snippet.html.erb):

<div id="<%= yield :id %>"><%= yield :id %>" class="snippet-container">
  <div class="snippet-title"><%= yield :title %></div>
  <div class="snippet-body"><%= yield :body %></div>
</div>

This layout expects three more content regions to be defined in the view: id, title and body. Here are the matching content regions from one sample view (for sponsors) — for brevity’s sake, I didn’t include all the views.

Sample module view (app/views/home/sponsors.html.erb):

<% content_for :id do %>sponsors<% end %>

<% content_for :title do %>Our Sponsors<% end %> 

<% content_for :body do %>
Please visit the sites of our wonderful sponsors!
<% end %>

Now, because of some nicely-placed classes and ids, I can once again use CSS selectors to give a common look-and-feel to all portlet containers (div.snippet-container), portlet titles (div.snippet-title) and to portlet bodies (div.snippet-body). Of course, if I want to diverge from the main look-and-feel, I can call out specific portlets: div.snippet-body#sponsors.

If I really want to get fancy, I can use CSS selectors to select, say, the sponsor portlet, but only when it’s running in the right column: table.main td.column-right div.snippet-container#sponsors.

So, in summary, using layouts, content_for and some crafty CSS, I can create a page of modules that can be styled generically or specifically. Combine this approach with what I described in Part 1, and you can “portal-ize” your Rails applications without using a portal!

Was this useful to you? If so, please leave a comment.

7 Replies to “Modular Page Assembly in Rails (Part 2)”

  1. The problem was that other views than index rendered without any layout. I’ve fixed the problem now by adding each action an @action variable that contains the actions name and in the layout, I made an if or case expression that looks up @action and then decides which layout to render. Isn’t really DRY, I’m sorry, but that way it works.
    I’ll post a link when it’s finished.
    Anyway, great plugin and a good tutorial, thanks!

  2. Sorry for not responding sooner. I’m not sure I fully understand your question, but I can tell you that there’s no way to implicitly render the views inside the master layout. Instead, I have effectively “hardcoded” references to each view I want to invoke in the index.html.erb — see app/views/company/index.html.erb for an example. Does that make sense?

    If I’m missing something here, maybe we can do a 10 minute screen share where you walk me through the problem and I try to understand it better.

  3. Hi again,
    sorry for pushing this, but could you tell me why my other views (except index) aren’t rendered inside the master layout and how I can let them render inside of it?
    Thanks,
    Daniel

  4. I had errors with routing and local variables that didn't show up in the next view. Routing problems seem to have been solved by adding the other views like the one you described:
    ———————————-
    each view of events_controller:
    views/events/new.html.erb:
    % content_for :event_content do %…% end %
    ———————————-
    one layout file per controller
    layouts/events.html.erb:
    div
    %= yield :event_content %
    /div
    ———————————-
    Some views obviously needed the locals to be accessed via [email protected], others didn't. Might be a newbie problem since this is my first more complex project with rails.
    Linking to views now works, but they don't render inside the master layout, instead they render completely without layout. Have I done sth wrong again?
    As I understood, the views should be rendered at the
    %= yield :left % command in the master layout, too.
    Or do I need to make an if-decision in the master view which :action to embed?
    I've also tried it with the nested_layouts_plugin, didn't work.
    Don't know what else to do.
    (sorry I had to delete the
    's in the source, HTML not accepted…)

  5. hi again, actually I do have a problem with your method. done everything as you described it, but I can’t access the controller’s different actions (for example: “link_to ‘Show’, event” doesn’t work). can you help me here?

Leave a Reply