63

I must be overlooking something very simple here but I can't seem to figure out how to render a simple ERB template with values from a hash-map.

I am relatively new to ruby, coming from python. I have an ERB template (not HTML), which I need rendered with context that's to be taken from a hash-map, which I receive from an external source.

However, the documentation of ERB, states that the ERB.result method takes a binding. I learnt that they are something that hold the variable contexts in ruby (something like locals() and globals() in python, I presume?). But, I don't know how I can build a binding object out of my hash-map.

A little (a lot, actually) googling gave me this: http://refactormycode.com/codes/281-given-a-hash-of-variables-render-an-erb-template, which uses some ruby metaprogramming magic that escapes me.

So, isn't there a simple solution to this problem? Or is there a better templating engine (not tied to HTML) better suited for this? (I only chose ERB because its in the stdlib).

Shrikant Sharat
  • 6,838
  • 9
  • 49
  • 74
  • I don't know of any Ruby template engines that are "tied" to HTML; a template is a template. Also not sure what's wrong with the solution you link to--is the issue getting the hash into the module? – Dave Newton Jan 21 '12 at 16:50
  • Dave, Nothing's wrong with it as such. Just that I thought there might be a more elegant solution for such a simple problem than to go to the extent of using metaprogramming. – Shrikant Sharat Jan 21 '12 at 17:15
  • 1
    this? http://stackoverflow.com/questions/1338960/ruby-templates-how-to-pass-variables-into-inlined-erb – tokland Jan 21 '12 at 17:57
  • Working example: https://gist.github.com/bastman/55f1c5a5bb474e472d5e – spuder Jul 20 '19 at 00:47

7 Answers7

78
require 'erb'
require 'ostruct'

def render(template, vars)
  ERB.new(template).result(OpenStruct.new(vars).instance_eval { binding })
end

e.g

render("Hey, <%= first_name %> <%= last_name %>", first_name: "James", last_name: "Moriarty")
# => "Hey, James Moriarty" 

Update:

A simple example without ERB:

def render(template, vars)
  eval template, OpenStruct.new(vars).instance_eval { binding }
end

e.g.

render '"Hey, #{first_name} #{last_name}"', first_name: "James", last_name: "Moriarty"
# => "Hey, James Moriarty

Update 2: checkout @adam-spiers comment below.

Moriarty
  • 3,118
  • 27
  • 24
  • 3
    +1, that's a super-elegant way to create a binding from a hash. – orip Jun 23 '13 at 11:11
  • 12
    Looks nice at first, but unfortunately the binding also inherits locals and methods from the context in which the `OpenStruct` is instantiated. This gives the template access to way more than intended, and could lead to bugs or even a security vulnerability, e.g. https://gist.github.com/aspiers/ad6549058ee423819976 – Adam Spiers Jul 20 '13 at 13:31
61

I don't know if this qualifies as "more elegant" or not:

require 'erb'
require 'ostruct'

class ErbalT < OpenStruct
  def render(template)
    ERB.new(template).result(binding)
  end
end

et = ErbalT.new({ :first => 'Mislav', 'last' => 'Marohnic' })
puts et.render('Name: <%= first %> <%= last %>')

Or from a class method:

class ErbalT < OpenStruct
  def self.render_from_hash(t, h)
    ErbalT.new(h).render(t)
  end

  def render(template)
    ERB.new(template).result(binding)
  end
end

template = 'Name: <%= first %> <%= last %>'
vars = { :first => 'Mislav', 'last' => 'Marohnic' }
puts ErbalT::render_from_hash(template, vars)

(ErbalT has Erb, T for template, and sounds like "herbal tea". Naming things is hard.)

Dave Newton
  • 152,765
  • 23
  • 240
  • 286
38

Ruby 2.5 has ERB#result_with_hash which provides this functionality:

$ ruby -rerb -e 'p ERB.new("Hi <%= name %>").result_with_hash(name: "Tom")'
"Hi Tom"
Tom Copeland
  • 906
  • 7
  • 9
32

If you can use Erubis you have this functionality already:

irb(main):001:0> require 'erubis'
#=> true
irb(main):002:0> locals = { first:'Gavin', last:'Kistner' }
#=> {:first=>"Gavin", :last=>"Kistner"}
irb(main):003:0> Erubis::Eruby.new("I am <%=first%> <%=last%>").result(locals)
#=> "I am Gavin Kistner"
Phrogz
  • 271,922
  • 98
  • 616
  • 693
8

The tricky part here is not to pollute binding with redundant local variables (like in top-rated answers):

require 'erb'

class TemplateRenderer
  def self.empty_binding
    binding
  end

  def self.render(template_content, locals = {})
    b = empty_binding
    locals.each { |k, v| b.local_variable_set(k, v) }

    # puts b.local_variable_defined?(:template_content) #=> false

    ERB.new(template_content).result(b)
  end
end

# use it
TemplateRenderer.render('<%= x %> <%= y %>', x: 1, y: 2) #=> "1 2"

# use it 2: read template from file
TemplateRenderer.render(File.read('my_template.erb'), x: 1, y: 2)
Lev Lukomsky
  • 5,617
  • 4
  • 26
  • 21
  • I am confused when you said "not to pollute binding with redundant local variables (like in top-rated answers)", as I did a test of the top accepted answer, and there where no extra local variables in the binding, specifically binding.local_variables.inspect returned just the variables expected. I agree your solution is a bit more efficient as OpenStruct uses method_missing. – iheggie May 14 '19 at 20:48
  • @iheggie it was polluted when I tested several years ago. Though it doesn't matter now since you can do this natively in ruby 2.5 eg. `ERB.new("Hi ").result_with_hash(name: "Tom")` – Lev Lukomsky May 15 '19 at 13:55
7

Simple solution using Binding:

b = binding
b.local_variable_set(:a, 'a')
b.local_variable_set(:b, 'b')
ERB.new(template).result(b)
asfer
  • 3,251
  • 1
  • 18
  • 16
4

If you want to do things very simply, you can always just use explicit hash lookups inside the ERB template. Say you use "binding" to pass a hash variable called "hash" into the template, it would look like this:

<%= hash["key"] %>
Alex D
  • 28,136
  • 5
  • 72
  • 115
  • But how do you use `binding` to pass the hash variable? – Adam Spiers Jul 20 '13 at 13:27
  • Easy. Just store the hash in a variable. Then in the same scope as the variable, call `ERB.new(template).result(binding)`. `Kernel#binding` will capture all the variables which are in scope, and they be available inside the ERB template. – Alex D Jul 20 '13 at 14:20
  • 2
    Thanks - this info belongs more in your main answer than in a comment. But doesn't this approach suffer from the same data leakage / security concerns which I noted above that the `instance_eval`-based answer suffers from? – Adam Spiers Jul 21 '13 at 14:25
  • 2
    Presumably you control the contents of your ERB templates, so security is not a concern. If the templates are untrusted, you have much bigger problems than "data leakage" caused by the use of `binding`. Since ERB templates can contain arbitrary Ruby code, you would have to look at sandboxing the whole process which renders the templates. (...which is clearly out of scope of the question asked here.) – Alex D Jul 21 '13 at 19:34
  • Yeah, good point - I guess it's not so much a security concern then, but there's still a risk of typos etc. accidentally referring to something outside the set of data intended for consumption by the template. – Adam Spiers Jul 22 '13 at 10:18