Turbo Rails Part 1：簡単な紹介 (Turbo Drive - Turbo Frames)


# Rails7 - Hotwire
<b>Hotwire (Stimulus + Turbo)</b> を利用すると <b>SPA</b>的なアプリを簡単に作れるようになります。
全てのRails７アプリは <b>Hotwire</b>が入っているので、すぐに使えます。

### Navigation: Turbo Drive
```js
// app/javascript/application.js
// Entry point for the build script in your package.json

import "@hotwired/turbo-rails"
import "./controllers"
```
App.jsを確認すると、デフォルトで `@hotwired/turbo-rails`はインポートされています。
この設定だけで、全てのリンクナビゲーションはAJAXリクエストに変わります。

リンクタッグでログイン画面移動してみましょう：
```ruby
<%= link_to "Log in", login_path %>
```
ログイン画面に移動して、ネットワークを確認したら、`fetch`リクエストになっています。
![Screenshot 2023-05-19 at 10.43.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3037285/bc102e31-2b7c-dea8-d9e7-be036e86bdb8.png)
特に何もせずにSPA的なナビゲーションはすでにできています。

#### どうやって？
リンク上の「クリック」イベントとフォーム上の「サブミット」イベントを<b>Turbo</b>が代わりに受信します。
例えば、擬似コードとして以下のようになります：

```js
// ページ上のすべてのリンクを選択
const links = document.querySelectorAll("a");

// 各リンクに「クリック」イベントリスナーを付けて、元の機能を切り替えます
links.forEach((link) => {
  link.addEventListener("click", (event) => {
    event.preventDefault()
    // ここからAJAXリクエストする
    // リスポンスで　<body></body> だけ切り替える
  }
)});
```

```js
// ページの上全てのフォームを選択
const forms = document.querySelectorAll("form");

// サブミットイベントをつけて、元の機能を切り替える。
forms.forEach((form) => {
  form.addEventListener("submit", (event) => {
    event.preventDefault()
    // ここからAJAXリクエストする
    // リスポンスで　<body></body> だけ切り替える
  }
)});
```

これはRails７アプリをスタートして、JSを書かずに成り立っています。
もちろん、Turbo Driveの機能は各リンクごと無効にできます。
(まだTurboDriveをサポートされてないGemはあるので)
```ruby
<%= link_to "Log in", login_path, data: { turbo: false } %>
```
実際にDOMを見ると、リンクは `data-turbo="false"`のアトリビュートが付けられている：
![Screenshot 2023-05-19 at 11.00.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3037285/3b5dc927-d067-2559-0994-89cec246d022.png)

ほとんどの場合、Turbo Drive は HTML ページの `<body>` だけを置き換え、`<head>` は変更されません。
(Turbo DriveにWebページの `<head>` の変更を認識させたい状況があります、例えばCSSや他の「asset」アプデートした時など)

実際に、app/views/layouts/application.html.erbを見ると：
```ruby
<%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
<%= javascript_include_tag "application", "data-turbo-track": "reload", defer: true %>
```
新しい要求があるたびに、現在のHTMLページの`<head>`および応答の`<head>`にあるDOM要素を`data-request-track="request"`と比較します。違いがある場合は、Turbo Driveがページ全体をリロードします。
面白いのは、コードを書かずにTurboとRailsがやってくれるところです。

### DOMとTurbo Frame
元のサーバアプリだけを使うと、DOM内に一個のノードをアップデートや追加するときに、ページ全体がリロードしたり、する必要があります。Turbo Frameを使うと、DOMを「Frame」の部分として分けられ、各「Frame」だけがアップデートできます。

#### どうやって？

Turbo Driveと同じ、AJAXでリクエストする時、TurboFrameの中のコンテンツしかサーバとやり取りしません。
簡単なイメージは：
![Untitled presentation.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3037285/5047b6ee-fb08-3859-d76d-69064a38e724.jpeg)

Turboは頭いい子なので、FrameのIDが与えている部分だけ切り替えてくれます。

### TurboFrameを使う際、いくつかのルールを覚えましょう：

<b>ルール1</b>: Turbo Frame内のリンクをクリックすると、Turboはターゲットページに同じIDのTurboFrameが表示されることを期待します。 次に、ソースページのTurbo FrameコンテンツをターゲットページのTurbo Frameコンテンツに置き換えます。

<b>ルール2</b>: Turbo Frame内のリンクをクリックすると、ターゲット ページに同じ ID のTurbo Frameがない場合、その Frameは消え、「応答に一致する <turbo-frame id="name_of_the_frame">」 要素がコンソールにエラーとして記録されます。

<b>ルール3</b>: リンクは、data-turbo-frame data アトリビュートのおかげで、直接ネストされている Frame
以外の Frameもターゲットにできます。

各ルールをもっとわかるように、小さいアプリを作ってみましょう：
```ruby
rails g scaffold Product title:string price:integer
```

##### ルール 1-2
```erb
<%# Product index.html.erb %>
<p style="color: green"><%= notice %></p>

<h1>Products</h1>

<div id="products">
  <% @products.each do |product| %>
    <%= render product %>
    <p>
      <%= link_to "Show this product", product %>
    </p>
  <% end %>
</div>

<%= turbo_frame_tag "first_turbo_frame" do %>
  <%= link_to "New product", new_product_path %>
<% end %>
```

上記見ると、`New Product`リンクを Turbo Frame (`turbo_frame_tag`) にラップします。
![Screenshot 2023-05-19 at 13.10.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3037285/e6cc4cea-04ff-cc27-21d1-501fec0996d9.png)

実際にリンクをクリックしたら：
![Screenshot 2023-05-19 at 13.15.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3037285/96dcc740-ef3a-1ab3-4b09-0634bb9227e8.png)


![Screenshot 2023-05-19 at 13.10.54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3037285/97733c98-1927-32cc-df95-c524e6e4cf87.png)

<b>ルール 1 - 2の通り、同じTurbo Frame IDを探して、なかったら「ない」というエラーが発生し、ページは「Content missing」が表示されます。</b>

この解決のために、`new.html.erb` のフォームも同じTurbo Frame ID でラップしてみましょう。
```erb
<%# Product new.html.erb %>
<h1>New product</h1>

<%# Product index.html.erb と同じ Turbo Frame tag id %>
<%= turbo_frame_tag "first_turbo_frame" do %>
  <%= render "form", product: @product %>
<% end %>

<br>

<div>
  <%= link_to "Back to products", products_path %>
</div>
```

これで実際に `/products` の中リンクをクリックすると：
![Screenshot 2023-05-19 at 13.27.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3037285/738e8d0c-6bb3-66a0-c9c5-035b3d6b581a.png)

![Screenshot 2023-05-19 at 13.20.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3037285/27dc84f4-4867-fdd8-25cd-f15599337b38.png)

<b>New product</b> リンクが<b>プロダクトフォーム</b>に切り替わります。URLもそのまま `/products`になっています。

##### ルール 3: リンクは、data-turbo-frame data アトリビュートのおかげで、直接ネストされているフレーム以外のフレームもターゲットにできます。

```erb
<%# Product index.html.erb %>
<p style="color: green"><%= notice %></p>

<h1>Products</h1>

<div id="products">
  <%= turbo_frame_tag "second_turbo_frame" do %>
    <% @products.each do |product| %>
      <%= render product %>
      <p>
        <%= link_to "Show this product", product %>
      </p>
    <% end %>
  <% end %>
</div>

<%= turbo_frame_tag "first_turbo_frame" do %>
  <%= link_to "New product", new_product_path, data: { turbo_frame: "second_turbo_frame" } %>
<% end %>
```
ちゃんと見ると、<b>products</b>がループされて、各 <b>product</b>をpartialで表示します。そして、そのブロックをTurbo Frame Tagでラップします。

```erb
<%# Product new.html.erb %>
<h1>New product</h1>

<%= turbo_frame_tag "second_turbo_frame" do %>
  <%= render "form", product: @product %>
<% end %>

<br>

<div>
  <%= link_to "Back to products", products_path %>
</div>
```
<b>product</b>作成のフォームも`second_turbo_frame`、同じIDで設定したら、プロダクト一覧がフォームに切り替えられます。

![Screenshot 2023-05-19 at 13.40.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3037285/198fb4f4-9bc6-f705-2f1f-876b9a54cd14.png)

![Screenshot 2023-05-19 at 13.39.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3037285/946e6399-9ddd-5015-c97b-896bb6b5bebe.png)

（もっとみやすくため、プロダクトを作っておきました）
ネストされたTurbo Frame Tagもターゲットできるし、同じIDがあれば、SPA的にDOMノードたちがページをリロードせずに切り替えてくれます。


#### TurboFrameでDOMアップデート
「Edit」する時にもページを移動せずにできたらもっと便利でしょう。なので、<b>Product</b>を「Edit」ところをTurbo Frameでやってみましょう。

「Create」フォームと同じく、「Edit」の方もTurbo Frame Tagでラップしないといけません。

今回、各プロダクトのIDで `turbo_frame_tag` を作ります。ユニークなIDを持っているFrameを作らないといけません。
```erb
<%# Products edit.html.erb %>
<h1>Editing product</h1>

<%= turbo_frame_tag "product_#{@product.id}" do %>
  <%= render "form", product: @product %>
<% end %>

<br>

<div>
  <%= link_to "Show this product", @product %> |
  <%= link_to "Back to products", products_path %>
</div>　

```
「Edit」フォームと同じIDを持つ `product partial`

```erb
<%# Products _product.html.erb %>
<%= turbo_frame_tag "product_#{product.id}" do %>
  <div id="<%= dom_id product %>">
    <p>
      <strong>Title:</strong>
      <%= product.title %>
    </p>

    <p>
      <strong>Price:</strong>
      <%= product.price %>
    </p>

  </div>
<% end %>
```

最後に「Edit」ページに移動するリンクのデータアトリビュートも同じ`turbo_frame`に設定したら、終わりです。
```erb
<%# Products show.html.erb %>
<p style="color: green"><%= notice %></p>

<%= render @product %>

<div style="margin-top: 1rem;">
  <%= link_to "Edit this product", edit_product_path(@product), data: { turbo_frame: "product_#{@product.id}" } %> |
  <%= link_to "Back to products", products_path %>
</div>
```

「Edit」リンクは設定した`turbo_frame_tag`と同じターゲットしているので、クリックをしたら、ページへの移動ではなく、`Show`の `render @product`部分だけ「Edit」フォームに 切り替わります。
![Screenshot 2023-05-19 at 14.25.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3037285/6b80c1c5-97b9-61ba-1050-0723536e24e0.png)

![Screenshot 2023-05-19 at 14.25.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3037285/a4d82b27-5bf7-a087-eb0f-d04c76ca84ca.png)

JSを書かずにSPAができています。


#### Turbo ID について
Turbo Frame Tagはデフォルトで `dom_id` のメソッドを使われるので、私たちが書いたコードを少しリファクタリングはできます：
例えば
```erb
<%# Products edit.html.erb %>
<h1>Editing product</h1>

<%= turbo_frame_tag @product do %>
  <%= render "form", product: @product %>
<% end %>

<br>

<div>
  <%= link_to "Show this product", @product %> |
  <%= link_to "Back to products", products_path %>
</div>
```
そのまま、「product」のインスタンスで設定したら、`turbo_frame_tag` がユニークなIDを作ってくれます。

### Turbo Rails Part 2: Turbo Streams + Broadcasting
また今度！