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