The dRuby Book

3.1 Generating Templates with ERB

You probably see lots of templates from day to day, used for things such as bills, meeting agendas, HTML, and domain-specific languages (DSLs) such as program generators.

In this chapter, we’ll learn various ways to generate templates using ERB.

Let’s look at a sample email first.

  ​-----------------------------------------​
  ​Qty Item​
  ​ Shipping Status​
  ​-----------------------------------------​
  ​1 Recollections of erb​
  ​ - Shipped June 22, 2008​
  ​1 Great BigTable and my toys​
  ​ - Shipped July 18, 2009​
  ​1 The last decade of RWiki and lazy me​
  ​ - Just shipped​
  ​-----------------------------------------​
  ​​
  ​You can print the receipt and check the status of this​
  ​order (and any of your other orders) online by visiting​
  ​your account at http://www.druby.org/m_seki​
  ​​
  ​http://www.druby.org​

If we want to turn the preceding example into a reusable template, we can combine String concatenation and a String literal, as follows:

shipping_notify_pre.rb
  class ShippingNotify​
  ​​
  def initialize​
  ​ @account = ''
  ​ @customer = ''
  ​ @items = []​
  end
  ​ attr_accessor :account, :customer, :items​
  ​​
  def to_s​
  ​ str = <<EOS
  Dear #{customer}​
  ​​
  This note is just to let you know that we've shipped
  some items that you ordered.
  ​​
  -----------------------------------------
  Qty Item
  Shipping Status
  -----------------------------------------
  EOS
  ​ items.each do |qty, item, shipped|​
  if shipped == Date.today​
  ​ status = 'Just shipped'
  elsif shipped < Date.today​
  ​ status = 'Shipped ' + shipped.strftime("%B %d, %Y")​
  else
  ​ status = 'NA'
  end
  ​ str << "#{qty}#{item}\n"
  ​ str << " - #{status}\n"
  end
  ​ str << <<EOS
  -----------------------------------------
  ​​
  You can print the receipt and check the status of this
  order (and any of your other orders) online by visiting
  Your Acount at http://www.druby.org/#{account}​
  ​​
  http://www.druby.org
  EOS
  return str​
  end
  end
  ​​
  if __FILE__ == $0​
  ​ greetings = ShippingNotify.new​
  ​​
  ​ greetings.account = 'm_seki'
  ​ greetings.customer = 'Masatoshi SEKI'
  ​ items = [[1, 'Recollections of erb', Date.new(2008, 6, 22)],​
  ​ [1, 'Great BigTable and my toys', Date.new(2009, 7, 18)],​
  ​ [1, 'The last decade of RWiki and lazy me', Date.today]]​
  ​ greetings.items = items​
  ​​
  ​ puts greetings.to_s​
  end

As you can see, lots of string literals are scattered across the script. There are two problems with embedding the partial templates into a script: it’s hard to read, and it’s hard to exchange scattered templates.

Many templating languages take an opposite approach. Rather than embedding documents into scripts, they embed scripts into documents, which is the approach eRuby takes.

eRuby is a templating system for embedding Ruby script into text files, and ERB is a Ruby library to write eRuby. There is also an “eruby” library implemented in C, but ERB is the one included in the Ruby standard libraries.

You can use eRuby not just to print out HTML but also to print out any text files (however, it won’t detect invalid HTML files).

Here’s how to embed a Ruby script:

<% ... %>

Execute the Ruby script.

<%= ... %>

Embed the result after evaluating the statement.

Other characters

Embed to strings as they are.

Not only can you use the result of the statement, but you can also control flow and iterations. Let’s rewrite the previous example using ERB. The following example splits the previous example into two programs. shipping_notify.erb is a template, and shipping_notify2.rb has all the data and logic.

shipping_notify.erb
  ​Dear <%= customer %>
  ​​
  ​This note is just to let you know that we've shipped​
  ​some items that you ordered.​
  ​​
  ​-----------------------------------------​
  ​Qty Item​
  ​ Shipping Status​
  ​-----------------------------------------<%
  items.each do |qty, item, shipped|
  if shipped == Date.today
  status = 'Just shipped'
  elsif shipped < Date.today
  status = 'Shipped ' + shipped.strftime("%B %d, %Y")
  else
  status = 'NA'
  end
  %>
  <%= qty %> <%= item %>
  ​ - <%= status %> <%
  end
  %>
  ​-----------------------------------------​
  ​​
  ​You can print the receipt and check the status of this​
  ​order (and any of your other orders) online by visiting​
  ​your account at http://www.druby.org/<%= account %>
  ​​
  ​http://www.druby.org​

The interesting code is around items.each. You can embed iterations and conditions into eRuby, not just string literals. Here is the refactored ShippingNotify class:

shipping_notify2.rb
Line 1  ​require 'erb'
class ShippingNotify​
def initialize​
​ @account = ''
​ @customer = ''
​ @items = []​
​ @erb = ERB.new(File.read('shipping_notify.erb')) ​
end
​ attr_accessor :account, :customer, :items​
10  ​​
def to_s​
​ @erb.result(binding)​
end
end
15  if __FILE__ == $0​
​ greetings = ShippingNotify.new​
​​
​ greetings.account = 'm_seki'
​ greetings.customer = 'Masatoshi SEKI'
20  ​ items = [[1, 'Recollections of erb', Date.new(2008, 6, 22)], ​
​ [1, 'Great BigTable and my toys', Date.new(2009, 7, 18)],​
​ [1, 'The last decade of RWiki and lazy me', Date.today]]​
​ greetings.items = items​
​​
25  ​ puts greetings.to_s​
end

Let’s compare the differences. First, the majority of templating strings are gone, except for data initialization into the items variable on line 20. Also, the initialize constructor has a statement to instantiate the ERB object on line 7.

  ​@erb = ERB.new(File.read('shipping_notify.erb'))​

ERB.new takes the eRuby script as an argument, converts it into a Ruby script, and then generates an ERB object that evaluates the Ruby object. In this example, you just created an ERB object from the shipping_notify.erb file. You can evaluate the ERB object as many times as possible, but we want to evaluate only once in this example, so we put it into the @erb variable inside the initialize method. Next, let’s look into the to_s method.

  def to_s​
  ​ @erb.result(binding)​
  end

The method calls the result method with a strange parameter called binding. binding is a predefined variable that includes all environmental information in the current scope, such as self, variables, and methods. This lets eRuby access instance variables and instance methods of the scope that calls the eRuby script.

In the preceding example, the eRuby script is evaluated in the same scope as ShippingNotify#to_s so that the eRuby script can access the account, customer, and items methods.

When you evaluate ERB, you can pass a binding. This means the eRuby script can access the application that uses eRuby. Thanks to this feature, you don’t have to worry about where to put support methods (the methods that the eRuby script uses) and how to pass them into the eRuby script.

Let’s move to the part of the shipping_notify.erb code that’s responsible for displaying these support methods within eRuby.

  if shipped == Date.today​
  ​ status = 'Just shipped'
  elsif shipped < Date.today​
  ​ status = 'Shipped ' + shipped.strftime("%B %d, %Y")​
  else
  ​ status = 'NA'
  end

These methods don’t fit properly with the rest of the code because the code has an if statement to calculate the shipping date.

Let’s extract them into a method and move them into the ShippingNotify class.

  class ShippingNotify​
  ​ ...​
  ​​
  def shipping_status(shipped)​
  if shipped == Date.today​
  'Just shipped'
  elsif shipped < Date.today​
  'Shipped ' + shipped.strftime("%B %d, %Y")​
  else
  'NA'
  end
  end
  ​​
  ​ ...​
  end

Then, let’s change the eRuby script to call this method.

  ​-----------------------------------------​
  ​Qty Item​
  ​ Shipping Status​
  ​-----------------------------------------<%
  items.each do |qty, item, shipped|
  ​%>​
  ​<%= qty %> <%= item %>
  - <%= shipping_status(shipped) %><%
  end
  ​%>​
  ​-----------------------------------------​

ShippingNotify is a View class to display text. It moves any string templating functions to an external eRuby script so that you can minimize mixing template and code. Looking at this from a different angle, the eRuby script moves any logic out of the template to make the template clean.

The following is the refactored code:

shipping_notify3.rb
  ​require 'erb'
  ​​
  class ShippingNotify​
  def initialize​
  ​ @account = ''
  ​ @customer = ''
  ​ @items = []​
  ​ @erb = ERB.new(File.read('shipping_notify3.erb'))​
  end
  ​ attr_accessor :account, :customer, :items​
  ​​
  def shipping_status(shipped)​
  if shipped == Date.today​
  'Just shipped'
  elsif shipped < Date.today​
  'Shipped ' + shipped.strftime("%B %d, %Y")​
  else
  'NA'
  end
  end
  ​​
  def to_s​
  ​ @erb.result(binding)​
  end
  end
  ​​
  if __FILE__ == $0​
  ​ greetings = ShippingNotify.new​
  ​​
  ​ greetings.account = 'm_seki'
  ​ greetings.customer = 'Masatoshi SEKI'
  ​ items = [[1, 'Recollections of erb', Date.new(2008, 6, 22)],​
  ​ [1, 'Great BigTable and my toys', Date.new(2009, 7, 18)],​
  ​ [1, 'The last decade of RWiki and lazy me', Date.today]]​
  ​ greetings.items = items​
  ​​
  ​ puts greetings.to_s​
  end

Let’s review what we’ve done so far.

When coding a template, we tend to mix lots of string concatenation inside the document so that we can scatter several pieces of code logic inside the template. It will become slightly better if we combine String concatenation and a String literal using a “here” document.[8] In our code sample, anything surrounded by “EOS” is treated as strings, even when the code goes across multiple lines. Both styles embed strings into the script. eRuby takes an opposite approach by embedding script into strings. ERB can evaluate eRuby with binding, so it is easier to integrate with your application without adding each variable you want to pass to the template into the eRuby script.

There are different opinions about how much eRuby should be in charge and how much Ruby should. If you try to remove as much logic as possible from your eRuby script, it will just become string replacement. If you introduce new syntax for iteration and condition statements, then it is almost the same as introducing a new language.

We learned one possible way of integrating an application with eRuby using binding. You can use other methods aside from result to evaluate ERB. Here are all the ways you can integrate with ERB (also see Figure 13, All the methods to evaluate the ERB object):

images/d2erb.png

Figure 13. All the methods to evaluate the ERB object
ERB.new(eruby_script, safe_level=nil, trim_mode=nil)

Generates an ERB object from eruby_script. You can specify the safe level (see more details about the safe level in Setting the Security Level with $SAFE) of eval and trim_mode (whether you trim the whitespace of the output or not).

run(b=TOPLEVEL_BINDING)

Runs ERB with binding and prints to standard output.

result(b=TOPLEVEL_BINDING)

Runs ERB with binding and returns a string.

src

Runs ERB with binding and returns the converted Ruby script.

Let’s try the src method.

  ​% irb -r erb​
  ​irb(main):001:0> ERB.new('Hello, World. <%= Time.now %>').src​
  ​=> "#coding:UTF-8\n_erbout = ''; _erbout.concat \"Hello, World. \";​
  ​ _erbout.concat(( Time.now ).to_s);​
  ​ _erbout.force_encoding(__ENCODING__)"​

The output is a bit hard to read, but it concatenates a string into the _erbout variable. When you run the result method, ERB evaluates this script. This is an expensive operation because you have to call eval, which executes every time the Ruby script is parsed. To avoid the repetitive call, you can turn the eRuby script into a method by wrapping the returning string with the method definition.

Let’s turn ShippingNotify into a method. You don’t have to change the eRuby script.

shipping_notify4.rb
  ​require 'erb'
  class ShippingNotify​
  def initialize​
  ​ @account = ''
  ​ @customer = ''
  ​ @items = []​
  end
  ​ attr_accessor :account, :customer, :items​
  ​​
  def shipping_status(shipped)​
  if shipped == Date.today​
  'Just shipped'
  elsif shipped < Date.today​
  'Shipped ' + shipped.strftime("%B %d, %Y")​
  else
  'NA'
  end
  end
  ​ extend ERB::DefMethod​
  ​ def_erb_method('to_s', 'shipping_notify3.erb')​
  end

Once you extend ERB::DefMethod, def_erb_method becomes available. This method takes a method name, an eRuby script filename, or the ERB object as arguments, and it defines the method into the environment where this method is called (the ShippingNotify class in this case).

shipping_notify4.rb
  ​extend ERB::DefMethod​
  ​def_erb_method('to_s', 'shipping_notify3.erb')​

The caller of def_erb_method is the ShippingNotify class. It defines the content of shipping_notify3.erb as a to_s method.

So far, we learned the basic of templating and how to use ERB. Separating some logic into its own class and passing it to ERB via binding is a good way to keep the template clean. In the next section, you’ll learn how to use ERB and dRuby with WEBrick::CGI so that you can publish your application via the Web.