He's not dead, he's resting

Feeding ERB Useful Variables: A Horrible Hack Involving Bindings

I’ve been playing around with Ruby to create Summer, a simple web packages thing for Exherbo. Originally I was hand-creating HTML output simply because it’s easy, but that started getting very very messy. Mike convinced me to give ERB a shot.

The problem with template engines with inline code is that they look suspiciously like the braindead PHP model. Content and logic end up getting munged together in a horrid, unmaintainable mess, and the only people who’re prepared to work with it are the kind of people who think PHP isn’t more horrible than an aborted Jacqui Smith clone foetus boiled with rotten lutefisk and served over a bed of raw sewage with a garnish of Dan Brown and Patricia Cornwell novels. So does ERB let us combine easy page layouts with proper separation of code?

Well, sort of. ERB lets you pass it a binding to use for evaluating any code it encounters. On the surface of it, this lets you select between the top level binding, which can only see global symbols, or the caller’s binding, which sees everything in scope at the time. Not ideal; what we want is to provide only a carefully controlled set of symbols.

There are three ways of getting a binding in Ruby: a global TOPLEVEL_BINDING constant, which we clearly don’t want, the Kernel#binding method which returns a binding for the point of call, and the Proc#binding method which returns a binding for the context of a given Proc.

At first glance, the third of these looks most promising. What if we define the names we want to pass through in a lambda, and give it that?

require 'erb'

puts"foo <%= bar %>").result(lambda do
    bar = "bar"

Mmm, no, that won’t work:

(erb):1: undefined local variable or method `bar' for main:Object (NameError)

Because the lambda’s symbols aren’t visible to the outside world. What we want is a lambda that has those symbols already defined in its binding:

require 'erb'

puts"foo <%= bar %>").result(lambda do
    bar = "bar"
    lambda { }

Which is all well and good, but it lets symbols leak through from the outside world, which we’d rather avoid. If we don’t explicitly say “make foo available to ERB”, we don’t want to use the foo that our calling class happens to have defined. We also can’t pass functions through in this way, except by abusing lambdas — and we don’t want to make the ERB code use rather than make_pretty(item). Back to the drawing board.

There is something that lets us define a (mostly) closed set of names, including functions: a Module. It sounds like we want to pass through a binding saying “execute in the context of this Module” somehow, but there’s no Module#binding_for_stuff_in_us. Looks like we’re screwed.

Except we’re not, because we can make one:

module ThingsForERB

puts"foo <%= bar %>").result(ThingsForERB.instance_eval { binding })

Now all that remains is to provide a way to dynamically construct a Module on the fly with methods that map onto (possibly differently-named) methods in the calling context, which is relatively straight-forward, then we can do this in our templates:

<% if summary %>
    <p><%=h summary %>.</p>
<% end %>


<table class="metadata">
    <% metadata_keys.each do | key | %>
            <th><%=h key.human_name %></th>
            <td><%=key_value key %></td>
    <% end %>


<table class="packages">
    <% package_names.each do | package_name | %>
            <th><a href="<%=h package_href(package_name) %>"><%=h package_name %></a></th>
            <td><%=h package_summary(package_name) %></td>
    <% end %>

Which gives us a good clean layout that’s easy to maintain, but lets us keep all the non-trivial code in the controlling class.


5 responses to “Feeding ERB Useful Variables: A Horrible Hack Involving Bindings

  1. Joseph April 16, 2009 at 3:33 am

    Aha! Very interesting. Just to play some golf with that approach:

    puts"foo ").result( => "bar").send(:binding))

    That is, OpenStruct might save you writing the proxy class.

  2. Pingback: SUMMER-time in Exherbo « Ingmar’s shameless self-blagvertisement.

  3. Nando Vieira August 3, 2011 at 12:38 pm

    An old post, I know, but you can do something like

    assigns = => "John Doe")
    assigns.extend(Helpers) #=> Helpers is just a module"").result assigns.instance_eval { binding }

    Note that you need to return the binding from inside the instance_eval call.

Leave a Reply

Fill in your details below or click an icon to log in: Logo

You are commenting using your account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s