Rails7のTurbo Framesの仕組み

# はじめに
本記事は錆びかけたRailsの知識を頑張ってアップデートするアドベントカレンダー6日目です。

引き続き猫Rails様の[猫でもわかるHotwire入門 Turbo編](https://zenn.dev/shita1112/books/cat-hotwire-turbo)を読み進めています。
Turbo Framesを使って一覧画面から編集/更新ができるように実装したところで挙動がややこしくなってきたので、自分なりにまとめます。

前提として「猫でもわかるHotwire入門」は非常にわかりやすく、悩むことなく実装を進められています。
その中でも「こう考えた方が理解しやすいかも？」と感じた部分を、後で読み返すためにまとめておきます。

併せて読んでいただくと役に立つ方もいるかもしれません。

# 前提
実現したいのは、一覧画面から更新ボタンを押したときに画面遷移せず、編集したい情報が表示されている部分が編集用フォームに置き換わるという挙動。

![girewaeeu.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/65439/b8a6fce8-79b5-0b3a-d4b9-8e8640c4bb12.gif)

## 実装方針
Turbo Driveではなく、Turbo Framesという機能を使う。
Turbo Driveは非同期での更新にbodyタグの中身を全て利用する。しかし、今回置き換えたいのは一覧表示の一部分である。そのため、ページの一部の要素を選択してそこだけを非同期で更新できるTurbo Framesを使う。

# 方法
一覧画面での編集/更新の基本的な考え方と、詳細な部分に分けて説明します。

## 基本的な考え方

- 非同期で更新したい**リクエスト側の**要素に目印をつける
- 置き換えたい要素にも目印をつける

### 非同期で更新したい**リクエスト側の**要素に目印をつける
今回の例で言えば、更新したいのは一覧表示の猫1匹分の部分です。
なので、`turbo_frame_tag`で囲むことで目印をつけてあげます。

```erb:_cat.html.erb
<%# 全体を`turbo_frame_tag`で囲う %>
<%= turbo_frame_tag cat do %>
  <div class="row py-2 border-top">
    <div class="col-4 my-auto">
      <%= cat.name %>
    </div>
    <div class="col-4 my-auto">
      <%= cat.age %>
    </div>
    <div class="col-4">
      <div class="d-flex justify-content-end">
        <%= link_to "編集", edit_cat_path(cat), class: "btn btn-sm btn-outline-primary me-2" %>
        <%= button_to "削除", cat, method: :delete, class: "btn btn-sm btn-outline-danger" %>
      </div>
    </div>
  </div>
<% end %>
```

この時、turbo_frame_tagヘルパーの引数にcatのインスタンスを渡すことで1つ1つの部分テンプレートに違ったid属性をつけ、更新したい要素を特定させます。

### 置き換えたい要素にも目印をつける
これでリクエスト時に「どこの要素を置き換えたいか」を伝えられるようになりました。伝える処理はTurbo Framesがよしなにやってくれます。続いて、その部分を「どれに置き換えたいか」も伝える必要があります。

どれに置き換えたいかについてもturbo_frame_tagを使って知らせます。今回はフォーム要素に置き換えるので、フォーム要素をturbo_frame_tagで囲います。ここでもturbo_frame_tagにcatのインスタンスを渡し、catの部分テンプレートと同様id属性をつけ、置き換えたい要素を同定させます。

```erb:_form.html.erb
<%# 全体を`turbo_frame_tag`で囲う %>
<%= turbo_frame_tag cat do %>
  <%= bootstrap_form_with(model: cat) do |form| %>
    <%# フォームの中身は省略 %>
  <% end %>
<% end %>
```

ここまでで、以下の処理は動くようになっています。

- 編集ボタンを押して更新フォームを非同期で出現させる
- 更新ボタンを押してフォームに書いた内容でデータを更新する(正常に保存できる場合)
- バリデーションによりエラーが発生する

一方で、以下の処理はまだ動きません。
- 更新フォームが出現した後にキャンセルボタンを押して元の表示に戻す

詳細にてそれぞれ説明します。

## 詳細

では先ほどのそれぞれの「なぜそうなるのか」を説明します。

### 編集ボタンを押して更新フォームを非同期で出現させる
編集ボタンを押したとき、その編集ボタンはturbo_frame_tagの範囲内なのでTurbo Framesのリクエストになります。
そしてこのリクエストはcats_controllerのeditアクションへのリクエストです。結果、編集ページの中に部分テンプレート`_cat.html.erb`があり、最初の手順でやった通りTurbo Framesの目印がついているためそこだけを非同期でレンダリングしてくれます。

### 更新ボタンを押してフォームに書いた内容でデータを更新する(正常に保存できる場合)
正常に保存できる場合、更新成功時にはcats_controllerの詳細画面(cats#show)へリダイレクトするような書き方にします。すると、詳細ページの中に部分テンプレート`_cat.html.erb`があるので、Turbo Framesの目印がついているためそこだけを非同期でレンダリングしてくれます。

```ruby:cats_controller.rb
# update
  def update
    if @cat.update(cat_params)
      # 成功時は/cats/:id（つまりcats#show）にリダイレクトする
      redirect_to @cat, notice: "ねこを更新しました。"
    else
      render :edit, status: :unprocessable_entity
    end
  end
```

### バリデーションによりエラーが発生する

Turbo Framesでバリデーションエラーが起きHTMLをレスポンスする時のステータスコードは`422 unprocessable_entity`にする必要があります。この時は編集画面(cats#edit)をレンダリングするよう返却するため、編集ページの中にある目印のついた部分テンプレート`_form.html.erb`のみが非同期でレンダリングされます。

### 更新フォームが出現した後にキャンセルボタンを押して元の表示に戻す
最後に、キャンセルボタンを押す挙動です。まずは、キャンセルボタンを追記します。

```diff_erb:_form.html.erb
     <div class="col-4">
       <div class="d-flex justify-content-end">
         <%= form.primary class: "btn btn-primary btn-sm me-2" %>
       + <%= link_to "キャンセル", cat, class: "btn btn-sm btn-outline-secondary" %>
       </div>
     </div>
   </div>
```

このキャンセルボタンはturbo_frame_tagで囲った範囲内にあるため、Turbo Frameのリクエストになります。リクエスト先はcats#showであり、「更新ボタンを押して正常に保存できる場合」と同様に詳細ページの中にTurbo Framesの目印がついた部分テンプレート`_cat.html.erb`があるためそこだけを非同期でレンダリングしてくれます。