Working with Templates in Ruby ERB

Templates in ERB

This post is a bit of a thought experiment. Looking into this led me down the route of some interesting ruby solutions.

The problem

Ruby ships with the erb library, but it natively doesn’t come with a way to wrap one ERB file in another. This makes composing files difficult. There’s no way to use something like rails’ yield method.

Some Basic ERB

Here’s how we’d render a simple erb template.

1
2
3
require 'erb'
template = "1 and 1 is <%= 1 + 1 %>"
ERB.new(template).result

This returns the interpreted string. If we try to use the yield keyword, we’ll run into a bit of a problem.

1
2
3
require 'erb'
template = "<%= yield %>"
ERB.new(template).result #=> LocalJumpError: no block given (yield)

Eek! No bueno. You might be thinking, “Well, you have to give a block to… something”. Good guess, but wrong.

1
2
3
4
5
6
7
8
9
require 'erb'
template = "<%= yield %>"
ERB.new(template).result do
  "HERE"
end
# or
ERB.new(template) do
  "HERE"
end.result

All sorts of crazy happens.

Defining a Renderer

ERB does come with a curious set of methods that let you add the current context of the ERB instance (stuff like the template, variables, all that stuff), to another object.

That looks like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
require 'erb'
# create a container for our new method
class LayoutRenderer
end

# load the erb instance
template = "Before <%= yield %> After"
erb = ERB.new(template)

# create method on LayoutRenderer. We'll pass a block to THIS method
erb.def_method(LayoutRenderer, 'render')

# Then call our LayoutRenderer
result = LayoutRenderer.new.render do
  "I'm somewhere in the middle"
end
result # => "Before I'm somewhere in the middle After"

Cool! Now it’s a matter of rendering another template inside of it. We’ll need a few files:

A layout:

1
2
3
4
5
6
7
8
9
10
11
  <!DOCTYPE html>
  <html lang="en">
  <head>
    <meta charset="UTF-8">
    <title>Document</title>
  </head>
  <body>
    <h1>Layout</h1>
    <%= yield %>
  </body>
  </html>

An index file:

1
2
3
<p>
  I'm in a template
</p>

And our renderer code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
require 'erb'

class LayoutRenderer
end

layout_template = File.read('layout.html.erb')
inner_template = File.read('index.html.erb')

layout = ERB.new(layout_template)
inner = ERB.new(inner_template)

layout.def_method(LayoutRenderer, 'render')

result = LayoutRenderer.new.render do
  inner.result # call the regular erb #result method
end

puts result

NOTE: If you’re playing with this and try to puts this line…

1
2
3
puts LayoutRenderer.new.render do
  inner.result
end

… you’ll get some weird issue. I think it’s trying to finish the render before loading the block.

Final Thoughts

This was fun to play with. Until next time, Happy Clacking.

comments powered by Disqus