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 = '' |
5 | @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):
-
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) ofeval
andtrim_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.