Ruby ERB and output_buffer

I just ran into this little frustration and thought others might, too. I have some template files that I need evaluated, and recently I wanted to add in some new functionality similar to the content_for method used in Rails templates, but in my own special way. I don't want them just rendered like Rails controllers because I don't need all that overhead for what are really some pretty small template files.

The funny thing about Rails is that if you try to go off the beaten path, everyone will ask why you're bothering to wander off, and shouldn't you just stay where it's comfortable? Probably 95% of the time they're right (and 3% of the rest of the time you realize six months later that they were probably right), but there are cases where it's useful to do something a little different (I think).

Anyway, most of the posts I saw on using ERB talked about its scary voodoo magic internals that aren't worth delving into. So I didn't. I had what I thought was a pretty basic problem, as follows:

I defined a method my_method that I wanted to call from within the templates which, for starters, I just wanted to assign to an instance variable. So I wrote:

def my_method(name, &block)
res = yield
self.instance_variable_set("@value_#{name}".to_sym, res.to_s.strip)
end

def parse!(template)
b = binding
self.body = ERB.new(template, 0, "%<>").result(b)
self.value_a = @value_a
self.body
end

Looked good to me, pretty simple. But if I passed in the template:

Start Template
<% my_method :a do %>
value for a
<% end %>
End Template

then I'd get:

== value for self.body ==
Start Template
value for a
End Template

== value for self.value_a ==
Start Template
value for a

This is what is generally considered bad, and not correct. I messed around with things for a while and got nowhere. Then I dug into how Rails was evaluating templates and its content_for method. ActionView::Base has an accessor called output_buffer being used by CaptureHelper (where content_for is defined) in a way I couldn't understand at first.

When you call content_for, part of its process is to store the value of output_buffer as old_output_buffer, then call its yield and finally re-set output_buffer to its original value. Weird, but at least it was a start. So I blindly added attr_accessor :output_buffer to my class and hoped for the best.

No love. I dug around a little more and found the rest of the trick. One of the arguments for ERB.new is the name of the local variable to use to store the string buffer used in the ERB evaluation. I'd never used that before because I didn't care, but if I set that value to "@output_buffer" then everything works magically.

So the new code is:

def my_method(name, &block)
old_buffer, @output_buffer = @output_buffer, ''
res = yield
self.instance_variable_set("@value_#{name}".to_sym, res.to_s.strip)
@output_buffer = old_buffer
end

def parse!(template)
b = binding
self.body = ERB.new(template, 0, "%<>", "@output_buffer").result(b)
self.value_a = @value_a
self.body
end

And suddenly everything works like a champ. Happy sigh.

Comments

Laura said…
i think my dad would love you! at least all your jargon. =) if you weren't working on your own thing i'd say L-3 needs, needs, NEEDS geniuses like you very badly.

Popular Posts