4

I'm implementing HTML templating in a Ruby project (non-rails). To do this I'll be using ERB, but I have some concerns about the binding stuff.

First out, this is the method I got so far:

def self.template(template, data)
  template = File.read("#{ENV.root}/app/templates/#{template}.html.erb")

  template_binding = binding.clone

  data.each do |k, v|
    template_binding.local_variable_set(k, v)
  end

  ERB.new(template).result(template_binding)
end

To call it I'll just do

Email.template('email/hello', {
  name: 'Bill',
  age:  41
}

There are two issues with the current solution though.

First, I'm cloning the current binding. I want to create a new one. I tried Class.new.binding to create a new, but since binding is a private method it can't be obtained that way. The reason I want a new one is that I want to avoid the risk of instance variables leaking into or out from the ERB file (cloning only takes care of the latter case).

Second, I want the variables passed to the ERB file to be exposed as instance variables. Here I tried with template_binding.instance_variable_set, passing the plain hash key k which complained that it wasn't a valid instance variable name and "@#{k}", which did not complain but also didn't get available in the ERB code. The reason I want to use instance variables is that it's a convention that the people relying on this code is familiar with.

I have checked some topics here at Stack Overflow such as Render an ERB template with values from a hash, but the answers provided does not address the problems I'm discussing.

So in short, like the title: How to create new binding and assign instance variables to it for availability in ERB?

Community
  • 1
  • 1
matsve
  • 297
  • 4
  • 11
  • I'm having a similar problem, that whenever I create a binding in Rails it has access to all my Rails ActiveRecord classes (User for example), and I don't want it to. I want it to have the same sort of environment as a new `irb` console would. Did you figure that out? – Max Williams Aug 15 '22 at 10:06

1 Answers1

2

1) No need to clone, new binding is created for you each time.

I have tested this in irb:

class A; def bind; binding; end; end
a = A.new
bind_1 = a.bind
bind_2 = a.bind

bind_1.local_variable_set(:x, 2)
=> 2
bind_1.local_variables
=> [:x]
bind_2.local_variables
=> []

2) Open the objects Eigenclass and add attr_accessor to it

class << template_binding  # this opens Eigenclass for object template_binding
  attr_accessor :x
end

So in ruby you can just open any class and add methods for it. Eigenclass means class of a single object - each object can have custom class definition. Coming from C# I couldn't imagine a situation where this would be used, until now. :)

Do this for each hash

data.each do |k, v|
  class << template_binding; attr_accessor k.to_sym; end
  template_binding.k = v
end
Marko Avlijaš
  • 1,579
  • 13
  • 27
  • Thanks Marko, that's really useful! I guess using the current binding would expose the current instance variables to the ERB code, but using something like what you do in the first line to create a dummy class from which to retrieve a new binding would fix that problem. – matsve Jan 11 '17 at 12:32
  • 1
    I don't understand your fear - that ERB code will add new instance variables to your object? That sounds impossible - try your scenario in IRB. Also upvote and accept the answer if it was helpful ;) – Marko Avlijaš Jan 11 '17 at 12:39
  • That was one of the fears, which I tried and, as you said, it does not seem to work that way. Also, the ERB code might get instance variables that it shouldn't. I want to be 100% explicit on what data gets passed to the ERB. Your answer helped me so I've upvoted and accepted it. Thanks a lot :) – matsve Jan 11 '17 at 12:50
  • I've come to this with a different fear, which is that I want to have erb templates exposed to our admin users, to write emails, that only give them access to pre-defined variables, and basic language features like if/else, but don't have access to the wider rails env, so they can't put <% User.delete_all %> or something. – Max Williams Aug 15 '22 at 09:10
  • The problem I have is that however I create a new binding it always has access to the wider environment, ie all the classes defined in Rails, whereas I want it to be more basic - the same sort of binding you would get if you just opened an 'irb' console. – Max Williams Aug 15 '22 at 09:34