Create a template rendering class with ERB

I’ve often wanted to create a class which generates some text from a template. I’ve had a go at this using Mustache but this time I wanted to see what can be done with ERB.

Just show me the code!

Here’s what we’ll end up with. Source available on GitHub.

class WelcomeMessage < ERB
  def self.template
    "Welcome, <%= @name %>"
  end

  def initialize(name, options = {})
    @name     = name
    @template = options.fetch(:template, self.class.template)
    super(@template)
  end

  def result
    super(binding)
  end
end

How ERB Works

First, I took a look at how Ruby’s ERB templating system works.

It’s pretty simple.

require 'erb'

name            = "Gareth"
template_string = "My name is <%= name %>"
template        = ERB.new(template_string)
template.result
# => prints "My name is Gareth"

So we can see all that’s needed to create a new instance of ERB is the template in the form of a String, and then call result in order to render the template and interpolate any variables.

Notice that name is a variable in the top level context of self.

The binding is a pointer to a set of local variables. If no binding is given, the top level binding will be used, which is why <%= name %> gets evaluated as “Gareth” in the above example.

We can pass a Binding object to ERB#render to access a different set of variables.

require 'erb'

class DummyController
  def index
    @name = 'Gareth'
  end

  # This is only a helper method to access
  # the object's (private) binding method
  def get_binding
    binding
  end
end

controller = DummyController.new
controller.index

template_string = "Welcome <%= @name %>"
template        = ERB.new(template_string)
template.result(controller.get_binding)
# => "Welcome Gareth"

Subclassing ERB

What I really wanted was a class that could return a rendered template itself.

First, I defined how I wanted to interact with the class and sketched a template of the public methods I’d need.

# Example usage
msg = WelcomeMessage.new('Gareth')
msg.result
# => "Welcome, Gareth"

# Sketch outline of class
class WelcomeMessage < ERB
  def initialize(name)
    # store the name of the person to greet
  end

  def result
    # render the template
  end
end

Setting up the data

After storing the name in an instance variable, the next step was picking the template to render. As we know, ERB itself takes the template string as an argument to initialize. We can pass this up the chain by calling super with the template. I set a default at the class level and allowed an option to be passed when creating a new instance of the class.

class WelcomeMessage < ERB
  def self.template
    "Welcome, <%= @name %>"
  end

  def initialize(name, options = {})
    @name     = name
    @template = options.fetch(:template, self.class.template)
    super(@template)
  end
end

# Use the default template
msg = WelcomeMessage.new('Gareth')
msg.instance_variable_get(:@template)
# => "Welcome, <%= @name %>"

# Use a custom template
msg = WelcomeMessage.new('Gareth', template: "Hello, <%= @name %>")
msg.instance_variable_get(:@template)
# => "Hello, <%= @name %>"

Now we need to interpolate the variables in to the template and render it. To make the API consistent with ERB, we’ll override ERB#result to always pass the binding as a parameter.

def result
  super(binding)
end

Now we can use the new class to render our template!

msg = WelcomeMessage.new('Gareth')
msg.result
# => "Welcome, Gareth"

Notes