Nokogiri::HTML::Builder

2013/10/23

Rails 誕生から 9 年が経過した現在(2013年10月)においても、私の周囲を見渡す限り(と言っても、ごく狭い範囲ですが) Rails 開発における HTML テンプレートエンジンの主流は相変わらず ERB です。もちろん HamlSlim も有名で、試されてはいるのですが、様々な理由により本格採用に至りません。

しかし、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 に条件分岐や繰り返しのロジックが含まれると、どうしてもソースコードが読みにくくなります。そこで、登場するのが NokogiriHTML::Builder です。Nokogiri のメインの機能は HTML/XML の解析ですが、HTML/XML コードを生成する機能も持ち合わせています。

HTML/XML のジェネレータとしては Rails 自体に含まれる builder という Gem パッケージもありますが、今回のケースでは Nokogiri::HTML::Builder の方が適しています。

準備作業

まず、Gemfilegem '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

参考資料

  1. http://nokogiri.org/Nokogiri/HTML/Builder.html
  2. Stackoverflow, http://stackoverflow.com/questions/4906681/using-nokogiri-html-builder-to-create-fragment-with-multiple-root-nodes, Asked on 2011-02-11