LEFログ

:smile

Railsで別ファイルに切り出さずに、Viewのプライベートなpartialを実装する

結論

captureメソッドとprocを使おう!

経緯

部分テンプレート(partial)はとても便利な機能ですが、使いすぎるとコードが少し散らかってしまいます。

たった1つのViewファイルで使うためだけの場合でも、_foo.html.erb_bar.html.erbといったファイルが増えてしまいます。それにもかかわらず、これらのファイルはどのViewからも呼び出せてしまいます(スコープが広いです)。

また、複数のファイルを行ったり来たりする手間もかかります。

例えばReactの場合では、単一ファイルの中でコンポーネントを分割することができます。このような機能をRailsのView(erb, haml, slim)でも実装する方法があるので共有します。

実装方法

結論に書いたように、captureメソッドとprocを使います。

captureメソッドについてはRailsガイドのこちらに説明があります。

railsguides.jp

captureメソッドを使うと、以下のようにテンプレートの一部を抽出して変数にキャプチャできます。

このcaputreメソッドを、procと組み合わせます。

docs.ruby-lang.org

2つを組み合わせて、次のようなコードにします。

<%# --- 定義:UIの断片を Proc オブジェクトに束ねる --- %>
<% row = proc do |user| %>
  <tr>
    <td><%= user.name %></td>
    <td><%= user.email %></td>
  </tr>
<% end %>

<%# --- 呼び出し: capture ヘルパーで Proc を評価・描画する --- %>
<table>
  <% @users.each do |user| %>
    <%= capture(user, &row) %>
  <% end %>
</table>

この方法によって、UIの断片を別ファイルに切り出すことなく、プライベートなpartialを実現できます。

もちろん、次のように複数の引数を渡すことも可能です。

<% row = proc do |user, book| %>
  <tr>
    <td><%= user.name  %></td>
    <td><%= book.title %></td>
  </tr>
<% end %>

<%= capture(current_user, featured_book, &row) %>

具体例

例えばこのようなerbを書きます。

<h1>Products</h1>

<%# --- 定義:テンプレート断片を Proc に束ねる --- %>
<% render_product = proc do |product| %>
  <div class="product">
    <h2><%= product.name %></h2>
    <p><%= product.description %></p>
    <span>¥<%= product.price %></span>
  </div>
<% end %>

<%# --- 1回目の呼び出し:通常の一覧表示 --- %>
<div id="products">
  <% @products.each do |product| %>
    <%= capture(product, &render_product) %>
  <% end %>
</div>

<%# --- 2回目の呼び出し:おすすめ商品セクション --- %>
<h2>Recommended Products</h2>
<div id="recommended">
  <% @products.select(&:recommended?).each do |product| %>
    <%= capture(product, &render_product) %>
  <% end %>
</div>

データを用意してCSSをつけると、次の画像のように表示されます。

プライベートなpartialが動いている画面

おわりに

この方法が一般的かは分かりませんが、一つのファイル内でプライベートなpartialを実装したいときに便利です。外部のGemや、複雑な実装をおこなうことなく、RubyRailsの基本的な機能のみで実現できます。

(もしかしたらもっと良い方法があるかもしれないので、お気軽にコメントください!)

えにしテック内のSlackでtmaedaさんとdarashiさんに頂いたコメントが、この記事のアイデアの元でした。この場を借りて感謝いたします🙏