7
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ソニックガーデン 若手プログラマAdvent Calendar 2024

Day 24

ViewComponentのSlotsをもっと知りたい

Last updated at Posted at 2024-12-23

はじめに

ViewComponent#Slots(2.12.0~)とは何か

親要素のみViewComponentにできます。
要素中に表示するものを任意に変更できるViewComponentが作成できる機能です。内部の子要素に依存しないViewComponentが作成できます。

以前困ったこと

画面

スクリーンショット 2024-12-15 20.00.04.png
↑ こういう投稿一覧があった

sample.jpg
↑ 投稿1件ずつの要素をページごとに別のHTMLで表現したい。だけど、見た目が同じだから共通化したいな...

具体的には...

  1. rootページではただの文字列として投稿を表示(既存の仕様)
  2. 投稿一覧ページでは投稿詳細へのリンクにしたい(追加の仕様)

生成したい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

全てのサンプルコード

今回のサンプルコードは公式ドキュメント↓から引用しています

元々のコード

app/views/blog_posts/index.html.erb
<div id="blog_posts" class="blog-posts">
  <%= render partial: "blogs", collection: BlogPost.all, 
                               locals: { title: "My blog" } %>
</div>
app/views/blog_posts/_blogs.html.erb
<%# locals: (title:, posts:) %>

<h1><%= link_to title, root_path %></h1>

<% posts.each do |post| %>
  <div>
    <%= post.name %>
  </div>
<% end %>

修正後のコード

| ViewComponentの生成
ViewComponentを生成するコマンド
$ rails generate component blog
| ページ表示のロジックを作成
app/components/blog_component.rb
# frozen_string_literal: true

class BlogComponent < ViewComponent::Base
  renders_one :header
  renders_many :posts
end

renders_one=> コンポーネント内に一つ表示したいslotの設定
renders_many => コンポーネント内に複数表示したいslotの設定

| テンプレートファイルを作成
app/components/blog_component.html.erb
<h1><%= header %></h1>

<% posts.each do |post| %>
  <%= post %>
<% end %>
| ViewComponentを表示したいページにレンダリング
app/views/blog_posts/index.html.erb
<%# 投稿一覧ページ %>
<%= 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 %>
app/views/home/index.html.erb
# 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に表示できます

うまく修正ができました:clap:
今までは子要素に依存して投稿リストを表示していました。
ViewComponent#Slotsを利用することで、子要素がどのようになっているかに関わらずリストとして表示できるコンポーネントを作成することができました!

(余談)パーシャルとViewComponent比較してみた

もしパーシャルで作るとしたら

| パーシャルを作成

app/views/blog_posts/_blogs.html.erb
<%# locals: (header:, posts:) %>

<h1><%= header %></h1>

<% posts.each do |post| %>
  <%= post %>
<% end %>

| パーシャルをレンダリング(もっと上手にパーシャルにもできると思いますが...)

app/views/blog_posts/index.html.erb
<% 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で表現できていますよね。

app/views/blog_posts/index.html.erb
+ <%= 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 が担当です。お楽しみに!

7
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?