はじめに
ViewComponent#Slots(2.12.0~)とは何か
親要素のみViewComponentにできます。
要素中に表示するものを任意に変更できるViewComponentが作成できる機能です。内部の子要素に依存しないViewComponentが作成できます。
以前困ったこと
画面
↑ 投稿1件ずつの要素をページごとに別のHTMLで表現したい。だけど、見た目が同じだから共通化したいな...
具体的には...
- rootページではただの文字列として投稿を表示(既存の仕様)
- 投稿一覧ページでは投稿詳細へのリンクにしたい(追加の仕様)
生成したいHTML
- ブログ一覧ページ
<div id="blog_posts" class="blog-posts">
<h1><a href="/">My blog</a></h1>
- <div>first-post</div>
- <div>second-post</div>
+ <a href="/blog/first-post">first-post</a>
+ <a href="/blog/second-post">second-post</a>
</div>
ViewComponentで解決してみた
前提
- Rails8.0
- Ruby 3.3.6
- ViewComponent 3.20.0
全てのサンプルコード
今回のサンプルコードは公式ドキュメント↓から引用しています
元々のコード
<div id="blog_posts" class="blog-posts">
<%= render partial: "blogs", collection: BlogPost.all,
locals: { title: "My blog" } %>
</div>
<%# locals: (title:, posts:) %>
<h1><%= link_to title, root_path %></h1>
<% posts.each do |post| %>
<div>
<%= post.name %>
</div>
<% end %>
修正後のコード
| ViewComponentの生成
$ rails generate component blog
| ページ表示のロジックを作成
# frozen_string_literal: true
class BlogComponent < ViewComponent::Base
renders_one :header
renders_many :posts
end
renders_one
=> コンポーネント内に一つ表示したいslotの設定
renders_many
=> コンポーネント内に複数表示したいslotの設定
| テンプレートファイルを作成
<h1><%= header %></h1>
<% posts.each do |post| %>
<%= post %>
<% end %>
| ViewComponentを表示したいページにレンダリング
<%# 投稿一覧ページ %>
<%= render BlogComponent.new do |component| %>
<% component.with_header do %>
<%= link_to "My blog", root_path %>
<% end %>
<% BlogPost.all.each do |blog_post| %>
<% component.with_post do %>
<%= link_to blog_post.name, blog_post.url %>
<% end %>
<% end %>
<% end %>
# Rootページ
<%= render BlogComponent.new do |component| %>
<% BlogPost.all.each do |blog_post| %>
<% component.with_post do %>
<div><%= blog_post.name %></div>
<% end %>
<% end %>
<% end %>
with_#{slot_name}
にブロックを渡すことでslotsに表示できます
うまく修正ができました
今までは子要素に依存して投稿リストを表示していました。
ViewComponent#Slotsを利用することで、子要素がどのようになっているかに関わらずリストとして表示できるコンポーネントを作成することができました!
(余談)パーシャルとViewComponent比較してみた
もしパーシャルで作るとしたら
| パーシャルを作成
<%# locals: (header:, posts:) %>
<h1><%= header %></h1>
<% posts.each do |post| %>
<%= post %>
<% end %>
| パーシャルをレンダリング(もっと上手にパーシャルにもできると思いますが...)
<% header = capture do %>
<%= link_to "My blog", root_path %>
<% end %>
<% posts = BlogPost.all.map do |blog_post| %>
<% capture do %>
<%= link_to blog_post.name, blog_post.url %>
<% end %>
<% end %>
<%= render "blogs", header:, posts: %>
| 呼び出し側のコードの比較
ViewComponentだと、投稿をうまくeachで表現できていますよね。
+ <%= render BlogComponent.new do |component| %>
+ <% component.with_header do %>
+ <%= link_to "My blog", root_path %>
+ <% end %>
+ <% BlogPost.all.each do |blog_post| %>
+ <% component.with_post do %>
+ <%= link_to blog_post.name, blog_post.url %>
+ <% end %>
+ <% end %>
+ <% end %>
- <% header = capture do %>
- <%= link_to "My blog", root_path %>
- <% end %>
- <% posts = BlogPost.all.map do |blog_post| %>
- <% capture do %>
- <%= link_to blog_post.name, blog_post.url %>
- <% end %>
- <% end %>
- <%= render "blogs", header:, posts: %>
パーシャルではcaptureメソッドを使っています
captureメソッドを使ってしまうと子要素内で条件分岐が生まれたときにロジックがERB内に増えて読みづらそう...(yieldとcontent_forでも書けそうですね)
最後に
ということでViewComponent#Slotsの紹介でした。
ViewComponent#Slotsを利用することで、ViewComponent内で子のViewcComponentを呼び出すこともできます。
うまい使い方はもっと他にもあると思いますが、ViewComponent自体すごく便利なのでぜひ使ってみてください。
↓ViewComponent内で他のViewComponentを呼ぶ
明日のアドベントカレンダーは @Hitoshi-Noborikawa が担当です。お楽しみに!