【Rails】ActionView::Helpers::CaptureHelper（content_for, yieldについて）

# はじめに

- content_for
- content_for?
- yield
- provide

あたりの理解を深めたかった。

特に `content_for` の挙動について理解を深めたかった。

# 概要

[Rails ActionView::Helpers::CaptureHelperのドキュメント](https://api.rubyonrails.org/classes/ActionView/Helpers/CaptureHelper.html)を追っていく。

メソッドは

- `capture`
- `provide`
- `content_for`
- `content_for?`

の4つが紹介されているが、
とりあえず `provide` , `content_for` , `content_for?` についてだけ読む。


## content_for(name, content = nil, options = {}, &block)

### 1. content_for の説明

`content_for` は、HTMLのブロックを後で使用できるように識別子に格納します。他のテンプレート、ヘルパーモジュール、またはレイアウトでこの格納されたコンテンツにアクセスするには、識別子を引数としてに渡します。

**注： `yield` も保存されたコンテンツを取得するのに使われていますが、ヘルパーモジュールで `yield` を呼び出しても機能しません。（ `content_for` は機能します。）**


```erb
<% content_for :not_authorized do %>
  alert('You are not authorized to do that!')
<% end %>
```

`content_for :not_authorized` は、テンプレートの任意の場所で使用できます。

```erb
<%= content_for :not_authorized if current_user.nil? %>
```

これは次と同等です。

```erb
<%= yield :not_authorized if current_user.nil? %>
```

 `content_for` はヘルパーモジュールでも使用できます。

```rb
module StorageHelper
  def stored_content
    content_for(:storage) || "Your storage is empty"
  end
end
```

このヘルパーは、通常のヘルパーと同じように機能します。

```erb
<%= stored_content %>
```

### 2. yield の説明

また、レイアウト内に `yield` がある場合でも `yield` 構文を使用することができます。


```erb
<%# This is the layout %>
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
  <title>My Website</title>
  <%= yield :script %>
</head>
<body>
  <%= yield %>
</body>
</html>
```

```erb
<%# This is our view %>
Please login!

<% content_for :script do %>
  <script>alert('You are not authorized to view this page!')</script>
<% end %>
```

```erb
<%= link_to 'Logout', action: 'logout', remote: true %>

<% content_for :script do %>
  <%= javascript_include_tag :defaults %>
<% end %>
```

これにより、JavaScriptのデフォルトのタグがページに配置されます。この手法は、これらのスクリプトをいくつかのビューでのみ使用する場合に役立ちます。

### 3. content_for の注意点1

`content_for` は特定の識別子に対して指定されたブロックを、順番に連結することに注意してください。


```erb
 <% content_for :navigation do %>
   <li><%= link_to 'Home', action: 'index' %></li>
 <% end %>

And in another place:

 <% content_for :navigation do %>
   <li><%= link_to 'Login', action: 'login' %></li>
 <% end %>
```

このコードは別のテンプレートまたはレイアウトで、両方のリンクを順番にレンダリングします。

```erb
<ul><%= content_for :navigation %></ul>
```

### 4. content_for の注意点2

引数 `flush`が `true` ならば、 `content_for` は与えられたブロックを置き換えます。

```erb
<% content_for :navigation do %>
  <li><%= link_to 'Home', action: 'index' %></li>
<% end %>

<%#  Add some other content, or use a different template: %>

<% content_for :navigation, flush: true do %>
  <li><%= link_to 'Login', action: 'login' %></li>
<% end %>
```

このコードは別のテンプレートまたはレイアウトで、最後のリンクのみをレンダリングします。

```erb
<ul><%= content_for :navigation %></ul>
```

### 5. 引数に渡せる値について

単純なコンテンツも引数として渡すことができます。

```erb
<% content_for :script, javascript_include_tag(:defaults) %>
```

※ `content_for` はキャッシュでは無視されます。そのため、フラグメントキャッシュされる要素には使用しないでください。

↑フラグメントキャッシュについては[別記事](https://qiita.com/yokoto/items/52a05bca505a30d64130)でまとめたので参照。

### 追記

`content_for` の実装を見ると、**引数がなければ表示する、引数があればキャプチャする** という風になっているっぽい。

```rb
# File actionview/lib/action_view/helpers/capture_helper.rb, line 155
def content_for(name, content = nil, options = {}, &block)
  if content || block_given?
    if block_given?
      options = content if content
      content = capture(&block)
    end
    if content
      options[:flush] ? @view_flow.set(name, content) : @view_flow.append(name, content)
    end
    nil
  else
    @view_flow.get(name).presence
  end
end
```

[Stack overflow](https://stackoverflow.com/questions/9185344/content-for-vs-yield-in-partials/10440291#10440291
)でも（ちょっと古いが）言及されてた。


## content_for?(name)

`content_for?` を使用すると、コンテンツがキャプチャされているかどうかを確認できます。ビューの内容に基づいてレイアウトの一部を異なる方法でレンダリングするのに役立ちます。

```erb
<%# This is the layout %>
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
  <title>My Website</title>
  <%= yield :script %>
</head>
<body class="<%= content_for?(:right_col) ? 'two-column' : 'one-column' %>">
  <%= yield %>
  <%= yield :right_col %>
</body>
</html>
```

## provide(name, content = nil, &block)

`content_for`と同じですが、例えば特定のテンプレートをレンダリングするとき、同じバッファに複数回連結したい時に使用したい場合は `content_for` を使用し、そうでない場合は `provide` を使用し、レイアウトが余分にコンテンツを検索しないようにしてください。

### 追記

これも `content_for` と同じで、引数がなければ表示、あればキャプチャ、という実装っぽい。

```rb
# File actionview/lib/action_view/helpers/capture_helper.rb, line 175
def provide(name, content = nil, &block)
  content = capture(&block) if block_given?
  result = @view_flow.append!(name, content) if content
  result unless content
end
```

## まとめ

- 大事だと思ったこと。
    1. `yield` と `content_for` の違い
    2. `content_for` に引数がある場合とない場合の挙動の違い