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.
class Template < OpenStruct
def method_missing(method, *args, &block)
respond_to?(method) ? self[method] : self[:_object].send(method, *args)
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, to which we supply the current execution context using the current binding.
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 :
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.