Template with Context using ERB

Recently, I was required to build a template rendering component that should render within a dynamic context, i.e. it would have methods / variables defined within a predefined context available during rendering, apart from what was provided directly.

For simplicity we chose ERB, since we can place them within locale files for internationalisation and designers could edit them with ease if need arises.

Following is a very simple implementation. Since we extend OpenStruct we can simply pass a hash to it during instantiation and all the keys will be accessible as methods and will be exposed directly during rendering.

require 'ostruct'

class Template < OpenStruct
  def render(string)
    ERB.new(string).result(binding)
  end

  def method_missing(method, *args, &block)
    respond_to?(method) ? self[method] : self[:_object].send(method, *args)
  end
end

The benefit of creating a separate abstraction is that it creates a sandbox environment for the template processing. This allows us to have more control over what gets exposed during processing and prevents accidental leakages into the context.

The main method here is the render method, where we utilise the ERB library. We create an ERB instance with the supplied template string and then call result (1)result Ruby Docs , to which we supply the current execution context using the current binding (2)binding Ruby Docs

The nifty trick here is the method_missing definition. Here we override the default definition which OpenStruct uses behind the scenes to access the keys as methods on the seed hash. We check for the existence of the method and delegate it to self (hash) when available, otherwise we delegate that method over to the special object available at :_object key, which serves as our ‘dynamic context’.

The usage would look something like this :

class TempObject
  attr_accessor :object_name
end
object = TempObject.new
object.object_name = 'object'

string = '<%= object_name %> and <%= real_name %>'

rendered_string = Template.new(real_name: 'string', _object: object).render(string)
#=> "object and string"

This gives us the flexibility of rendering the given template in any context and hence the templates could be designed flexibly. Since we built this as a rubygem, it allows us to expose template processing in a way that allows the host application to build it’s own context with custom functions / attributes to be used during rendering.

comments powered by Disqus