ViewComponentのSlotsをもっと知りたい

# はじめに
### ViewComponent#Slots(2.12.0~)とは何か
**親要素のみ**ViewComponentにできます。
要素中に表示するものを任意に変更できるViewComponentが作成できる機能です。**内部の子要素に依存しない**ViewComponentが作成できます。

# 以前困ったこと
### 画面
![スクリーンショット 2024-12-15 20.00.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3327469/fa40af56-1a4b-18b6-a05e-31ab747a7992.png)
↑ こういう投稿一覧があった

![sample.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3327469/b5abd469-b636-c336-10b4-5df7914cf3f5.jpeg)
↑ 投稿１件ずつの要素をページごとに別のHTMLで表現したい。だけど、見た目が同じだから共通化したいな...

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

### 生成したいHTML
* ブログ一覧ページ
```diff_erb:こうしたい
<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

**全てのサンプルコード**

https://github.com/kohey69/view_component_app

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

https://viewcomponent.org/guide/slots.html

### 元々のコード
```erb: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>
```

```erb: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の生成
```shell:ViewComponentを生成するコマンド
$ rails generate component blog
```
##### | ページ表示のロジックを作成
```rb:app/components/blog_component.rb
# frozen_string_literal: true

class BlogComponent < ViewComponent::Base
  renders_one :header
  renders_many :posts
end
```
:::note info
`renders_one`=> コンポーネント内に一つ表示したいslotの設定
`renders_many` => コンポーネント内に複数表示したいslotの設定
:::

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

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

##### | ViewComponentを表示したいページにレンダリング
```erb: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 %>
```

```erb: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 %>
```
:::note info
`with_#{slot_name}`にブロックを渡すことでslotsに表示できます
:::

https://github.com/kohey69/view_component_app/pull/2/commits/eec199a8251059e65adee74431a0d8e4443791a5

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

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

#### もしパーシャルで作るとしたら
#### | パーシャルを作成
```erb:app/views/blog_posts/_blogs.html.erb
<%# locals: (header:, posts:) %>

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

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

#### | パーシャルをレンダリング(もっと上手にパーシャルにもできると思いますが...)
```erb: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: %>
```

https://github.com/kohey69/view_component_app/pull/2/commits/06eda5ee132ec8540c13dd5b570099bb12c93b7e

#### | 呼び出し側のコードの比較
ViewComponentだと、投稿をうまくeachで表現できていますよね。

```diff_erb: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を呼ぶ

https://viewcomponent.org/guide/slots.html#component-slots

明日のアドベントカレンダーは @Hitoshi-Noborikawa が担当です。お楽しみに！