Turbo Frame とTurbo Stream とは？Turbo Rails Tutorial をやってみた（4章）

本記事は、🔽「Turbo Rails Tutorial」の学習を進めていく際の個人メモです！

https://www.hotrails.dev/turbo-rails

🔽 前章の記事はこちら

https://qiita.com/yuppymam/items/e3fef1a153ddcd78927c

# 4章 [Turbo Frames and Turbo Stream templates](https://www.hotrails.dev/turbo-rails/turbo-frames-and-turbo-streams)

3章までは、Turbo Drive により、ページ推移にリロードが発生しなくて高速になる、ということまで分かりました。4章では、Turbo Frame と Turbo Stream を使って、`new`アクションや`edit`アクションを別ページに推移せず、同一のページで行えるようにします。

まずは、期待する動作になるよう、テスト（`test/system/quotes_test.rb`）を変更しました。

## Turbo Frames とは？

Webページの独立した部分で、JavaScriptを1行も記述せずに、追加、前置、置換、または削除することができます。

### `turbo_frame_tag`

Turbo Frame を作成するには、`turbo_frame_tag`を使います。
🔽 `/quotes`ページのこの部分につけます。
![スクリーンショット 2023-02-15 21.36.54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/293962/73418b4d-bcd0-0f1c-9f77-329239806f8a.png)

```erb: app/views/quotes/index.html.erb
<%= turbo_frame_tag "first_turbo_frame" do %>
  <div class="header">
    <h1>見積もり一覧</h1>
    <%= link_to "見積もりを作成",
                new_quote_path,
                class: "btn btn--primary" %>
  </div>
<% end %>
```

🔽 生成されたHTMLには、`first_turbo_frame` id がついた`turbo-frame`タグが生成されています。

```html
<turbo-frame id="first_turbo_frame">
    <div class="header">
      <h1>見積もり一覧</h1>
      <a class="btn btn--primary" href="/quotes/new">見積もりを作成</a>
    </div>
</turbo-frame>
```
チュートリアルでは、この状態で「見積もりを作成」ボタンを押すとフレームが消えてブラウザにエラー（`Response has no matching <turbo-frame id="first_turbo_frame">element`）が出る、とありましたが、エラーは出ず`/quotes/new`ページへ正常に移動しました。また、その際リロードが発生していました。

次に、`/quotes/new`ページの以下の部分に、`/quotes`ページにつけた id と同じ`turbo_frame_tag`をつけてみます。

![スクリーンショット 2023-02-15 22.20.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/293962/2c10ccca-5274-5966-1706-7b5436da0cf3.png)

すると...！

![turbo_frame_tag.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/293962/7009aa50-d69c-28a7-63b8-4d821928e4ba.gif)

URLは`/quotes`のまま、同じ id で紐づいた`turbo_frame_tag`のHTMLが置き換わりました！

チュートリアルでは、`turbo_frame_tag`の id を変えてみて、マッチしない場合はエラーが出るとありましたが、やはりAjaxではない通常のリクエストになるだけでした。

#### `turbo_frame_tag`の考察
2. Turbo はリンク先のページに同じ id のフレームがないか探す
3. リンク先のページに同じ id があれば、Turbo Frame のコンテンツをリンク先ページの Turbo Frame コンテンツに置き換える
4. リンク先のページに同じ id がない場合は、通常のリクエストになる？

### `data-turbo-frame`
Turbo Frame で囲っているHTML内のリンクは、`data-turbo-frame`属性に別の Turbo Frame の`id`を指定することで、他の`Turbo Frame`を動作させることもできます。

![output.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/293962/f4cd87e4-4b58-95be-f52a-5aa3103f4bf2.gif)

`/index`の一覧部分と、`/new`の`form`部分を`"second_frame"`という`id` の Turbo Frame で紐づけておき、以下のように`data`属性で指定すると、「見積もりを作成」をクリックした時`"second_frame"` `id`の Turbo Frame が動作するようになっています。

```erb: app/views/quotes/index.html.erb
<%= turbo_frame_tag "first_turbo_frame" do %>
  <div class="header">
    <h1>見積もり一覧</h1>
    <%= link_to "見積もりを作成",
                new_quote_path,
                data: { turbo_frame: "second_frame" },
                class: "btn btn--primary" %>
  </div>
<% end %>
```

#### ページ全体を表す`_top`という特別なフレーム
先ほどの`data-turbo-frame`属性に`_top`という特別なキーワードを使うと、Turbo Frame ではなくページ全体の更新となります。URLも変わるようになり、デフォルトの Turbo Drive の動作のようになりました。

![top.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/293962/bb2ce385-8192-0108-8415-808e310497cb.gif)

これは、すべてのページにデフォルトで「ページ全体を Turbo Frame 化」したものが`_top`として備わっているからだそうです。


```erb: app/views/quotes/index.html.erb
<%= turbo_frame_tag "first_turbo_frame" do %>
  <div class="header">
    <h1>見積もり一覧</h1>
    <%= link_to "見積もりを作成",
                new_quote_path,
                data: { turbo_frame: "_top" },
                class: "btn btn--primary" %>
  </div>
<% end %>
```

### `dom_id`
例えば、一覧ページでそれぞれの見積もりをクリックして「編集」させたい場合、それぞれの`id`で指定して`edit`ページの`turbo_frame_tag`に紐付けすることができます。

こんな感じの`turbo_frame_tag`が作成できます。
```erb: app/views/quotes/edit.html.erb
<%= turbo_frame_tag "quote_#{@quote.id}" do %>
  ...
<% end %>
```
しかし、上記のコードでは文字列を使っており、スペルミスによりバグが入り込みやすいです。そこで、`dom_id`ヘルパーを使うことで、上記のコードと全く同じものを作成しながら、格段にすっきりと書けます。

```erb: app/views/quotes/edit.html.erb
<%= turbo_frame_tag dom_id(@quote) do %>
  ...
<% end %>
```

さらに、`dom_id`ですらも省略が可能で、最終的にこうなります。
```erb: app/views/quotes/edit.html.erb
<%= turbo_frame_tag @quote do %>
  ...
<% end %>
```

`dom_id`、一意のIDに変換するのに便利ですね。

```rb
dom_id(@quote) # => "quote_1"

dom_id(Quote.new) # => "new_quote"

# 任意の文字列を連結させることもできる
dom_id(Quote.new, "prefix") # "prefix_new_quote"
```

`dom_id`については、以下の記事にも詳しく載っています。

https://techracho.bpsinc.jp/hachi8833/2022_07_06/119249

## Turbo Streams とは？

削除ボタンを押してみると、`TURBO_STREAM`というものが表示されるのが確認できます。Rails7のフォームは、`TURBO_STREAM`形式で送信されるそうです。
```
Started DELETE "/quotes/309456473" for ::1 at 2023-04-23 00:43:00 +0900
Processing by QuotesController#destroy as TURBO_STREAM
```

先ほどから利用してきた`turbo_frame_tag`では、リンクのクリック時と、フォームの送信時に1ヶ所の書き換えはできるのですが、それ以外の操作は`turbo_stream`を使います。

今回のチュートリアルで言うと、ある見積もりの「編集」を押して`edit`フォームに書き換えられたページで、他の見積もりの「削除」を行う場合は、`destroy`アクション内で`turbo_stream`を使います。

```rb: app/controllers/quotes_controller.rb
def destroy
  @quote.destroy

  respond_to do |format|
    format.html { redirect_to quotes_path, notice: "Quote was successfully destroyed." }
    format.turbo_stream
  end
end
```
`respond_to`で JavaScript が無効化されている場合でも通常のフォーム送信で削除ができるようになっています。

ブラウザの JavaScript を無効/有効で切り替えて動作を試すと、面白いです！

## 4章で完成したもの
この章で学んだものを色々と組み合わせて、最終的にこんなページができました！

![ch4fin.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/293962/9b99180b-0bf7-ec07-a028-66e4b9d2e675.gif)

同時に編集ができるの、気持ちいですね！
「やめる」ボタンの挙動も、追加コードはたったの１行。よく噛み砕かないと裏側で何が起きているのか理解するのは結構難しかったですが、分かると楽しいですね。

（にしても、JavaScriptを1行も書かずにこれ実現できているの、凄い！！）

これにて4章は終わりです。次は5章をやっていきます。Action Cable を使ってリアルタイムアップデートを作るそうです。楽しみ〜🙌

https://www.hotrails.dev/turbo-rails/turbo-streams