Nokogiri::HTML::Builder
2013/10/23
Rails 誕生から 9 年が経過した現在(2013年10月)においても、私の周囲を見渡す限り(と言っても、ごく狭い範囲ですが) Rails 開発における HTML テンプレートエンジンの主流は相変わらず ERB です。もちろん Haml や Slim も有名で、試されてはいるのですが、様々な理由により本格採用に至りません。
しかし、ERB では書きにくいケースというのが明らかに存在します。
例えば、次のような HTML の断片を ERB で生成することを考えましょう:
<table id="users">
<tr>
<th>Family Name</th>
<th>Given Name</th>
</tr>
<tr class="admin">
<td>Yamada</td>
<td>Taro</td>
</tr>
<tr>
<td>Suzuki</td>
<td>Jiro</td>
</tr>
<tr>
<td>Tanaka</td>
<td>Saburo</td>
</tr>
</table>
前提として、User というモデルクラスの存在を想定しています。そして、users テーブルには
family_name(String)given_name(String)admin(Boolean)
という3つのカラムが定義されているものとします。
ポイントは6行目です。
ユーザーの admin カラムの値が true であれば、tr 要素の class 属性に admin という値を指定したいということです。
このことを考慮に入れなければ、ERB で次のように書けますね:
<table id="users">
<tr>
<th>Family Name</th>
<th>Given Name</th>
</tr>
<% @users.each do |u| %>
<tr>
<td><%= u.family_name %></td>
<td><%= u.given_name %></td>
</tr>
<% end %>
</table>
さて、ここからどうしましょうか。
直接的な解法
もっとも単純で直接的な解法は、<% ... %> の間に class 属性を埋め込んでしまうというものです:
<table id="users">
<tr>
<th>Family Name</th>
<th>Given Name</th>
</tr>
<% @users.each do |u| %>
<tr<% if u.admin? %> class="admin"<% end %>>
<td><%= u.family_name %></td>
<td><%= u.given_name %></td>
</tr>
<% end %>
</table>
これで一応はうまく行くのですが、けっして読みやすいとは言えません。
ヘルパーメソッド content_tag
第二の解法は、Rails 標準のヘルパーメソッド content_tag を使用するものです:
<table id="users">
<tr>
<th>Family Name</th>
<th>Given Name</th>
</tr>
<% @users.each do |u| %>
<%= content_tag(:tr, :class => u.admin? ? 'admin' : nil) do %>
<td><%= u.family_name %></td>
<td><%= u.given_name %></td>
<% end %>
<% end %>
</table>
少し改善されたような気がしますね。「三項演算子が嫌い」という方は、次の別解を:
<table id="users">
<tr>
<th>Family Name</th>
<th>Given Name</th>
</tr>
<% @users.each do |u| %>
<% attrs = {} %>
<% attrs[:class] = 'admin' if u.admin? %>
<%= content_tag(:tr, attrs) do %>
<td><%= u.family_name %></td>
<td><%= u.given_name %></td>
<% end %>
<% end %>
</table>
ちょいと長くなりましたが、コードの意図は伝わりやすくなったかと思います。
Nokogiri::HTML::Builder
さて、ERB に条件分岐や繰り返しのロジックが含まれると、どうしてもソースコードが読みにくくなります。そこで、登場するのが Nokogiri の HTML::Builder です。Nokogiri のメインの機能は HTML/XML の解析ですが、HTML/XML コードを生成する機能も持ち合わせています。
HTML/XML のジェネレータとしては Rails 自体に含まれる builder という Gem パッケージもありますが、今回のケースでは Nokogiri::HTML::Builder の方が適しています。
準備作業
まず、Gemfile に gem 'nokogiri' という1行を追加して、bundle install します。
続いて、app/lib ディレクトリを(なければ)作成し、そこに新規ファイル html_builder.rb を次のような内容で作成します:
module HtmlBuilder
def markup
root = Nokogiri::HTML::DocumentFragment.parse('')
Nokogiri::HTML::Builder.with(root) do |doc|
yield(doc)
end
root.to_html.html_safe
end
end
このソースコードの説明は割愛させていただきます。
そして、app/helpers/application_helper.rb を次のように修正します:
module ApplicationHelper include HtmlBuilder end
以上の準備作業を終えると、ヘルパーメソッド markup が利用可能となります。
ヘルパーメソッド markup の使い方
ヘルパーメソッド markup を利用すると、先ほどのテーブルは次のコードで生成できます:
markup do |m|
m.table(id: 'users') do
m.tr do
m.th 'Family Name'
m.th 'Given Name'
end
@users.each do |u|
attrs = {}
attrs[:class] = 'admin' if u.admin?
m.tr(attrs) do
m.td u.family_name
m.td u.given_name
end
end
end
end
このコードは HTML テンプレートの <% ... %> に直接埋め込むことができます。
<%=
markup do |m|
m.table(id: 'users') do
...
end
end
%>
あるいは、次のようにヘルパーメソッドとして定義して、
module ApplicationHelper
include HtmlBuilder
def table_of_users(users)
markup do |m|
m.table(id: 'users') do
m.tr do
m.th 'Family Name'
m.th 'Given Name'
end
users.each do |u|
attrs = {}
attrs[:class] = 'admin' if u.admin?
m.tr(attrs) do
m.td u.family_name
m.td u.given_name
end
end
end
end
end
end
ERB テンプレートから呼び出すことも可能です:
<%= table_of_users(@users) %>
おわりに
Nokogiri::HTML::Builder を利用して HTML の断片を生成する Ruby コードの方が、元の ERB コードよりも読みやすいと思うかどうかは、人によるのかもしれません。
しかし、table 要素全体を Ruby コードで書けるようになったので、様々な工夫の余地が出てきます。例えば、この連載の第1回で紹介した Presenter のメソッドにするとか、さらにコードが複雑になってきたら複数のメソッドに分解するとか…。
ERB テンプレートがごちゃごちゃしてきたと思ったら、いちどお試しください。
追記(2013-10-23)
記事を発表した直後、もっと HtmlBuilder.markup のソースコードがもっと簡潔に書けることに気付き、本文を修正しました。
修正前のソースコードは以下の通り:
module HtmlBuilder
def markup
builder = Nokogiri::HTML::Builder.new do |doc|
doc.root { yield(doc) }
end
ActiveSupport::SafeBuffer.new.tap do |buffer|
builder.doc.root.children.each do |node|
buffer.safe_concat node.to_html
end
end
end
end
