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.
1 2 3 4 5 6 7 8 9 10 11 12 | require 'erb' 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, 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 :
1 2 3 4 5 6 7 8 9 10 | 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.