Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails関連

Rails: ViewComponentの「スロット」を極めるための活用ガイド(翻訳)

概要

元サイトの許諾を得て翻訳・公開いたします。

日本語タイトルは内容に即したものにしました。

Rails: ViewComponentの「スロット」を極めるための活用ガイド(翻訳)

viewcomponent/view_component - GitHub

GitHubが手掛けているViewComponentは、オープンソースプロジェクトとして広く人気を集めています。ViewComponentの目的は、Railsの複雑なビューを整然と分解して、Railsアプリケーションのパフォーマンスを強化することです。

スロット(slot)はViewComponentの強力な機能の1つです。スロットを使えば、シンプルなテキスト出力から複雑なコンポーネントのレンダリングまで何でもこなせます。それではスロットのしくみを見ていきましょう。

🔗 ViewComponentのスロットとは

ViewComponentのスロット設計は強力です。ネストしたコンテンツを渡してレンダリングすることはもちろん、他のネストしたコンポーネントを渡してレンダリングすることすら可能なので、コンポーネントが柔軟かつ再利用可能になります。

"スロットV2" API(#503)の改良版として導入されたスロットは、今やViewComponentのデフォルトの機能となっているので、もしまだお使いでなければ、最初の段階から使うことを私からも強くおすすめいたします。スロットは、私のRails Designerでも広く使われています。


それでは、どんな種類のスロットがあるかを見ていきましょう。スロットで普通のコンテンツをレンダリングするもよし、他のコンポーネントをレンダリングするもよし、さらにはポリモーフィックなスロットも利用可能と、自由自在です。

スロットの定義方法は2通りあります。

  • renders_one
  • renders_many

どちらもその名前からの想像通りに動作します。

renders_oneは、単一のスロットのみをレンダリング(定義)できます(UserProfileComponentにアバターを表示する場合など)。

renders_manyは、複数のスロットをレンダリングできます(ArticleComponentの中で多数の関連リンクを表示するなど)。

以下で説明するスロットは、renders_onerenders_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_avatarwith_icon_avatarのように、typeハッシュ内のキー名(imageicon)を中間に追加して呼び出します。

🔗 5: スロット名?述語メソッド

「えー、また新しい用語?」はい、しかしこれはスロット名の末尾に?を追加しただけの述語メソッド(truefalseだけを返すメソッド)です。

これまでと同様の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で知っておきたいスロットの知識は以上でおしまいです。
オプションがいろいろあるので、どの種類のスロットを選んだら良いのか戸惑うかもしれませんが、まずはオプションをそれぞれ試してみてから次に進むのが、スロットのさまざまなオプションを使いこなす早道です。

もっとサンプルコードが欲しいなどのご質問がありましたら、お気軽にお問い合わせください

関連記事

Rails: ViewComponentで最初に作るのは「ダイアログコンポーネント」がおすすめ

Rails: パーシャルよりもViewComponentを選ぶべき理由(翻訳)


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。