What's New on Edge Merb: Big ERB Changes

June 24, 2008 | Reporter: wycats

We try hard not to break public APIs in Merb, but in the lead-up to 1.0, we'll probably be doing a bit more breakage than we have in the past in an effort to reduce breakage post 1.0. Today is one of those days. In order to improve the way ERB handles blocks, we were forced to make a substantial change that I will detail here.

We try hard not to break public APIs in Merb, but in the lead-up to 1.0, we’ll probably be doing a bit more breakage than we have in the past in an effort to reduce breakage post 1.0.

Today is one of those days. In order to improve the way ERB handles blocks, we were forced to make a substantial change that I will detail here.

First, some background.

The Dirty Truth

Frequently, you’ll want to write a helper that takes a block. An example of such a helper is the form_for helper, which looks something like this in Rails:


<% form_for :foo do |f| %>
  <%= f.text_field(:zoo) %>
<% end %>

The semantics you want are something like this:

  1. Capture the contents of the block (&lt;%= f.text_field(:zoo) %>)
  2. Create some boilerplate (@@)
  3. Concatenate the results of the entire thing into the buffer

But wait! As it turns out, those are only the semantics you want if you are being called from ERB. Let’s take another example, the gentle content_tag helper.

In ERB, it works like this:


<% content_tag(:div, :id => "foo") do %>
  Hello, my name is Foo
<% end %>

The above semantics still work just fine, so all is well. But lots of people use content_tag in regular Ruby code, as in:


  def link(text, url)
    content_tag(:a, :href => url) { text }
  end

which will be called from ERB as:


  <%= link("A Link", "http://example.com/foo") %>

Now look back over the semantics below. Step 3 is “Concatenate the results of the entire thing into the buffer”. But wait. What buffer? If we do a concat onto the ERB buffer, the results of the #link function will still get concatenated, as well as the results of the content_tag function.

I won’t go any deeper, but suffice it to say that the fundamental problem here is that helpers that take a block cannot simply return a string. In other words, the following doesn’t work:


<%= form_for :foo do |f| %>
  <%= f.text_field(:zoo) %>
<% end %>

The reasons for this are kind of arcane, but the bottom line is that since it doesn’t work, we’re forced to use the concat hack, which then has the problem of having to deal with figuring out whether it was called from ERB (and thus should concat) or a regular helper (and thus should return a String).

How Does Rails Do It?

When confronting issues such as these, the first step is usually to investigate how Rails handles the problem. In general, they do tackle the problem in some way or other, and so it’s usually worth figuring out what their approach is.

Let’s whip up the latest tag_helper.rb on Rails edge and pull up content_tag


def content_tag(name, content_or_options_with_block = nil, options = nil, escape = true, &block)
  if block_given?
    options = content_or_options_with_block if content_or_options_with_block.is_a?(Hash)
    content_tag = content_tag_string(name, capture(&block), options, escape)

    if block_called_from_erb?(block)
      concat(content_tag)
    else
      content_tag
    end
  else
    content_tag_string(name, content_or_options_with_block, options, escape)
  end
end  

The important part is the middle section, beginning with block_called_from_erb?. Let’s take a look at the implementation of block_called_from_erb?:


BLOCK_CALLED_FROM_ERB = 'defined? __in_erb_template'

# Check whether we're called from an erb template.
# We'd return a string in any other case, but erb <%= ... %>
# can't take an <% end %> later on, so we have to use <% ... %>
# and implicitly concat.
def block_called_from_erb?(block)
  eval(BLOCK_CALLED_FROM_ERB, block)
end

So effectively, every time you use content_tag, or any helper that uses content_tag, Rails executes an eval inside the block to check whether __in_erb_template is defined. This is sort of an acceptable solution, but it means that any helper that does a concat needs to handle this case.

The solution

Feel free to skip this section if you just want to kno

I alluded to the solution above:


<%= form_for :foo do |t| %>
  <%= f.text_field(:zoo) %>
<% end %>

If this would compile, then all would be well. Unfortunately, Erubis (like ERB) doesn’t support multiline. As I dug into why, it became messier and messier. To get a sense, the following two cases need to be treated differently:


<%= form_for :foo do |t| %>
  <%= f.text_field(:zoo) %>
<% end %>

<% if @foo %>
  <%= @bar %>
<% end %>

The first case needs to be compiled into something like @_out_buf << (form_for(:foo) {|f| @_out_buf << f.text_field(:zoo)} ).to_s. The second case gets compiled into if @foo; @_out_buf << (@bar).to_s; end.

The bottom line is that with a lot of hacking, I was able to get Erubis to compile blocks correctly, in the following form.


<%= form_for :foo do |t| %>
  <%= f.text_field(:zoo) %>
<% end =%>

Note the =%> on the last line. That tells Erubis that this end is the end of a block opened up via <%= previously. This provides a bit of symmetry from multi-line expression in Erubis (they open in <%= %> and close in <% =%>).

The Bottom Line

Effective today’s edge, helpers that take blocks are to be instantiated via the <%= %>...<% =%> pattern. If you make a new helper that takes a block, use #capture to get the contents of the block, and simply return a String. Let that sink in for a moment. Helpers that take blocks simply return a String.

Another Note

Also coming very soon to edge are completely rebuilt Form Helpers. The new helpers were refactored with modularity in mind, making it very easy to create your own Form classes to use in your application with as little code as possible. Stay tuned for more on this.

Comment On What's New on Edge Merb: Big ERB Changes

On June 24, 2008 at 10:15 nanodeath says:

The last bit of that went way over my head, but this is pretty cool. Seems like a Good Idea to return strings whenever possible, especially if it means avoiding unnecessary evals. I’m impressed with how stable the API has been since I started using it a couple months ago; I think this is the first breaking change I’ve seen so far. Fortunately it’s an easy fix for me :) Keep up the good work guys.

On August 23, 2008 at 20:54 lastobelus says:

It is a good refactor in isolation; it is unfortunate however that it will make sharing views between rails & merbs apps very tedious and annoying. Because you have to change both ends of a recursive element, writing a regex to do it will be tricky and beyond the initiative/skill of the average tirekicker. A reliable translator would need to be based on an erb parser, and since you can still do blocks the old way the translator would need only/except lists of blocks to transform, so that you weren’t required to refactor all the rails plugins you were using in order to use the translator.

I don’t know how big a goal it is for you to promote adoption of merb but this will surely dampen it much more than some of the other differences.

On August 23, 2008 at 23:12 wycats says:

@lastobelus The problem is that the way Rails handles it requires an eval for each and every use of tag helpers et al. That’s simply unacceptable, especially in the face of emerging Ruby implementations (like Rubinius and JRuby) which have significantly more expensive evals.

Additionally, this simplifies the task of making Merb helpers, instead of having to know to check for whether you’re inside ERB, and know about having to use concat (which to a lot of new Rails/Merb developers, is very difficult to grok at first).

On August 24, 2008 at 15:27 lastobelus says:

I totally agree that it is better. Hopefully someone will get an itch to make a rails-merb view translator.

I’m really enjoying working with merb, by the way. In the last couple days I’ve had to develop a small auxiliary application that won’t grow much which I took as an opportunity to convince my product-owner was a good time to try merb. I really like the run_later function, and though there is far less googlable info about merb than rails this is offset by how much easier it is to understand and work with the framework code. Except for apps that are basically rails lego, I will probably be using merb a lot.

Sign in to make your voice heard