Switch_off Switch_on Switch_off
Inventive Labs: Web Problem Solvers

Like that but with this: view inheritance in Rails [05092007]

Yesterday I wrote about a simple method that lets you to place default content in your Rails layouts. This default content can be optionally overridden by your views using the standard-issue content_for.

It's handy enough on its own, but it's also a piece of a somewhat larger puzzle — something I've called view inheritance. The principle is that, just as Ruby classes can extend from parent classes, so ERB templates should be able to extend from parent templates.

Consider this template, a.html.erb:

<html>
<body>
<% default_content_for :title do %>
  <h1>Bonjour</h1>
<% end %>
</html>
</body>

Now b.html.erb, which inherits a.html.erb:

<% inheriting_view 'a' do %>
  <% content_for :title do %>
    <h1>Aloha</h1>
  <% end %>
<% end %>

That says "do everything that the first template does, but make the title say 'Aloha'".

Of course we're not doing much different from what a layout does yet — we've just shifted responsibility for specifying the layout file from the controller (or outer view) to the view itself. Sometimes that's not very useful, and although view inheritance can be wielded as a general replacement for layouts, I'm not really recommending it.

But what about this scenario? You have a model X, and an XController that serves up a form for X. Then you need a model Y, which is like X but with a few extra fields. So you create the Y model:

class Y < X

And perhaps a YController:

class YController < XController

But how do you add the extra fields to the form? Like this:

<% inheriting_view 'x/form' do %>
  <% content_for :additional_fields do %>
    <fieldset>
      <legend>Geography</legend>
      <label>Latitude
        <%= @f.text_field('latitude') %>
      </label>
      <label>Longitude
        <%= @f.text_field('longitude') %>
      </label>
    </fieldset>
  <% end %>
<% end %>

So DRY you'll need a beer when you're done.

Here's the code:

  def inheriting_view(options = {}, &block)
    # We accept a shorthand syntax -- if options is a string, render as a file.
    return inheriting_view({:file => options}, &block) if options.is_a?(String)

    bind = options[:binding]

    # Get our differences and additions to the view we're inheriting.
    if block_given?
      bind ||= block.binding
      instance_variable_set(:@content_for_layout, capture(&block)) 
    end

    raise "Important: inheriting_view() requires a block. " +
      "An empty block (eg, using {}) is suitable." unless bind 

    # If we're inheriting a partial, lend our local context to that partial.
    options[:locals] = eval("local_assigns", bind) if options[:partial]

    # Render our parent view.
    concat(render(options), bind)
  end

(Yes, this works for partials inheriting other partials too.)


Michael Edwards [Wed 23 Apr 2008, 12:39PM] said:

This is great, but I can't link the fields to the actual form block that is in the original view, I get undefined local variable or method `f' for #

when trying <%= f.text_field :isbn %>

in a 'content_for' block

Joseph [Wed 23 Apr 2008, 12:51PM] said:

If you want to pass variables between the views, make them instance variables. ie, replace your declaration of the f form object with @f. I'll update the example in the post.

If you can't modify the original view for some reason, you could also just use the older form helpers that don't rely on a form object.

Only the comment field is required. Omitting the ID fields increases your risk of being mistaken for spam.

Preview or