Rails: ViewComponentの「スロット」を極めるための活用ガイド(翻訳)
GitHubが手掛けているViewComponentは、オープンソースプロジェクトとして広く人気を集めています。ViewComponentの目的は、Railsの複雑なビューを整然と分解して、Railsアプリケーションのパフォーマンスを強化することです。
スロット(slot)はViewComponentの強力な機能の1つです。スロットを使えば、シンプルなテキスト出力から複雑なコンポーネントのレンダリングまで何でもこなせます。それではスロットのしくみを見ていきましょう。
🔗 ViewComponentのスロットとは
ViewComponentのスロット設計は強力です。ネストしたコンテンツを渡してレンダリングすることはもちろん、他のネストしたコンポーネントを渡してレンダリングすることすら可能なので、コンポーネントが柔軟かつ再利用可能になります。
"スロットV2" API(#503)の改良版として導入されたスロットは、今やViewComponentのデフォルトの機能となっているので、もしまだお使いでなければ、最初の段階から使うことを私からも強くおすすめいたします。スロットは、私のRails Designerでも広く使われています。
それでは、どんな種類のスロットがあるかを見ていきましょう。スロットで普通のコンテンツをレンダリングするもよし、他のコンポーネントをレンダリングするもよし、さらにはポリモーフィックなスロットも利用可能と、自由自在です。
スロットの定義方法は2通りあります。
renders_onerenders_many
どちらもその名前からの想像通りに動作します。
renders_oneは、単一のスロットのみをレンダリング(定義)できます(UserProfileComponentにアバターを表示する場合など)。
renders_manyは、複数のスロットをレンダリングできます(ArticleComponentの中で多数の関連リンクを表示するなど)。
以下で説明するスロットは、renders_oneとrenders_manyのどちらでも同様に使えます。
🔗 1: コンテンツをレンダリングする
以下は最も基本的なスロットの定義方法です。
# user_profile_component.rb
class UserProfileComponent < ViewComponent::Base
renders_one :avatar
end
<%# user_profile_component.html.erb %>
<header>
<%= avatar %>
<h1><%= @user.name %></h1>
</header>
これで、このUserProfileComponentをビューでレンダリングするときに、以下のようにavaterスロットをwith_avatarブロックで定義できます。
<%= render UserProfileComponent.new do |component| %>
<% component.with_avatar do %>
<%= image_tag("path/to/avatar/of/sorts.jpg", alt: "") %>
<% end %>
<% end %>
なお、with_avatarには任意のブロックを渡せるので、以下のように{ }ブロックで書くことも可能です。
<%= render UserProfileComponent.new do |component| %>
<% component.with_avatar { image_tag("path/to/avatar/of/sorts.jpg", alt: "") } %>
<% end %>
🔗 2: 別のコンポーネントを渡してレンダリングする
スロットを介して別コンポーネントをレンダリングする方法は、「コンポーネントクラスの内部でレンダリングする方法」と「別ファイルとしてレンダリングする方法」の2通りがあります。
🔗 2-1: 別コンポーネントをインラインでレンダリングする
# user_profile_component.rb
class UserProfileComponent < ViewComponent::Base
renders_one :avatar, "AvatarComponent"
class AvatarComponent < ViewComponent::Base
def call
tag.img src: "path/to/avatar/of/sorts.jpg", alt: ""
end
end
end
上のrenders_one :avatar, "AvatarComponent"行で、"AvatarComponent"のようにコンポーネントのクラス名が引用符で囲まれていることにお気づきでしょうか?
ここがポイントで、このようにコンポーネント名を文字列として追加すると、ViewComponentはそのコンポーネントが親コンポーネント内でネストしていると仮定します。
🔗 2-2: 別ファイルにあるコンポーネントをレンダリングする
# user_profile_component.rb
class UserProfileComponent < ViewComponent::Base
renders_one :avatar, AvatarComponent
end
# avatar_component.rb
class AvatarComponent < ViewComponent::Base
def call
tag.img src: "path/to/avatar/of/sorts.jpg", alt: ""
end
end
この場合のrenders_one :avatar, AvatarComponent行では、AvatarComponentというクラス名をを引用符で囲まずに参照しているので、ViewComponentはこのAvatarComponentを別ファイルから探し出します。
🔗 3: lambdaスロットをレンダリングする
lambdaスロットだなんて、ちょっと怖そうですね。しかしlambdaとは、名前のない関数(無名関数)を->()でその場で作成して渡せる、ただのミニ関数です。
それでも不安でしたら、以下のサンプルを見るとわかりやすいでしょう。
# user_profile_component.rb
class UserProfileComponent < ViewComponent::Base
renders_one :avatar, ->(src: nil, alt: alt, css: "w-6 h-6 rounded-full") do
content_tag(:img, src: src, alt: alt, class: css)
end
end
続いてUserProfileComponentをレンダリングします。
<%= render UserProfileComponent.new do |component| %>
<% component.with_avatar(src: "path/to/avatar/of/sorts.jpg", alt: "My Profile picture") %>
<% end %>
このぐらいシンプルな要素なら普通に動作します。
しかしブロック内で別のコンポーネントを呼び出すことも可能です。以下の例では、上で説明したインラインコンポーネントと外部コンポーネントを両方使っています。
# user_profile_component.rb
class UserProfileComponent < ViewComponent::Base
renders_one :avatar, ->(src: nil, alt: nil, css: "w-6 h-6 rounded-full") do
AvatarComponent.new(src: src, alt: alt, css: css)
end
end
なお、上のAvatarComponentは、src属性とalt属性、オプションのcss属性の3つの引数を受け取ることを前提としています。
🔗 4: ポリモーフィックなコンポーネントをレンダリングする
ポリモーフィックなスロットがViewComponentに追加されたのは、比較的最近のことでした(2.12.0以後)。私自身が見出したユースケースはまだ多くありませんが、それでも何度か使ってみました。以下の少々わざとらしい例を見てみましょう。
# user_profile_component.rb
class UserProfileComponent < ViewComponent::Base
renders_one :avatar, types: {
icon: ->(css: "w-4 h-4 rounded-full") do
tag.span @user.first.name, class: css
end,
image: ->(css: "w-4 h-4 rounded-full") do
image_tag @user.avatar, class: css
end
}
end
これで、ビューで以下のように書けます。
<%= render UserProfileComponent.new(user: Current.user) do |component| %>
<% if Current.user.avatar.attached? %>
<% component.with_image_avatar %>
<% else %>
<% component.with_icon_avatar %>
<% end %>
<% end %>
従来ならwith_avatarを呼んでいたところを、ポリモーフィックスロットではwith_image_avatarやwith_icon_avatarのように、typeハッシュ内のキー名(imageやicon)を中間に追加して呼び出します。
🔗 5: スロット名?述語メソッド
「えー、また新しい用語?」はい、しかしこれはスロット名の末尾に?を追加しただけの述語メソッド(trueかfalseだけを返すメソッド)です。
これまでと同様のUserProfileComponentで説明します。
ユーザーのアバター画像を親要素でラップする必要があるとします。このとき、avatarスロットは省略可能だとします。
するとERBテンプレートで、avatarが定義されている場合にのみ表示するようアバター用HTMLを以下のようにラップできます。
<%# user_profile_component.html.erb %>
<header>
<% if avatar? %>
<div class="mr-4">
<%= avatar %>
</div>
<% end %>
<h1><%= @user.name %></h1>
</header>
ViewComponentで知っておきたいスロットの知識は以上でおしまいです。
オプションがいろいろあるので、どの種類のスロットを選んだら良いのか戸惑うかもしれませんが、まずはオプションをそれぞれ試してみてから次に進むのが、スロットのさまざまなオプションを使いこなす早道です。
もっとサンプルコードが欲しいなどのご質問がありましたら、お気軽にお問い合わせください。
概要
元サイトの許諾を得て翻訳・公開いたします。
日本語タイトルは内容に即したものにしました。