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.