LoginSignup
2
3

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

Posted at

Rails7 - Hotwire

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

Navigation: Turbo Drive

// 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リクエストに変わります。

リンクタッグでログイン画面移動してみましょう:

<%= link_to "Log in", login_path %>

ログイン画面に移動して、ネットワークを確認したら、fetchリクエストになっています。
Screenshot 2023-05-19 at 10.43.20.png
特に何もせずにSPA的なナビゲーションはすでにできています。

どうやって?

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

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

// 各リンクに「クリック」イベントリスナーを付けて、元の機能を切り替えます
links.forEach((link) => {
  link.addEventListener("click", (event) => {
    event.preventDefault()
    // ここからAJAXリクエストする
    // リスポンスで <body></body> だけ切り替える
  }
)});
// ページの上全てのフォームを選択
const forms = document.querySelectorAll("form");

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

これはRails7アプリをスタートして、JSを書かずに成り立っています。
もちろん、Turbo Driveの機能は各リンクごと無効にできます。
(まだTurboDriveをサポートされてないGemはあるので)

<%= link_to "Log in", login_path, data: { turbo: false } %>

実際にDOMを見ると、リンクは data-turbo="false"のアトリビュートが付けられている:
Screenshot 2023-05-19 at 11.00.03.png

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

実際に、app/views/layouts/application.html.erbを見ると:

<%= 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

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

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

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

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

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

各ルールをもっとわかるように、小さいアプリを作ってみましょう:

rails g scaffold Product title:string price:integer
ルール 1-2
<%# 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

実際にリンクをクリックしたら:
Screenshot 2023-05-19 at 13.15.41.png

Screenshot 2023-05-19 at 13.10.54.png

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

この解決のために、new.html.erb のフォームも同じTurbo Frame ID でラップしてみましょう。

<%# 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

Screenshot 2023-05-19 at 13.20.35.png

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

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

ちゃんと見ると、productsがループされて、各 productをpartialで表示します。そして、そのブロックをTurbo Frame Tagでラップします。

<%# 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>

product作成のフォームもsecond_turbo_frame、同じIDで設定したら、プロダクト一覧がフォームに切り替えられます。

Screenshot 2023-05-19 at 13.40.03.png

Screenshot 2023-05-19 at 13.39.04.png

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

TurboFrameでDOMアップデート

「Edit」する時にもページを移動せずにできたらもっと便利でしょう。なので、Productを「Edit」ところをTurbo Frameでやってみましょう。

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

今回、各プロダクトのIDで turbo_frame_tag を作ります。ユニークなIDを持っているFrameを作らないといけません。

<%# 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

<%# 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に設定したら、終わりです。

<%# 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と同じターゲットしているので、クリックをしたら、ページへの移動ではなく、Showrender @product部分だけ「Edit」フォームに 切り替わります。
Screenshot 2023-05-19 at 14.25.40.png

Screenshot 2023-05-19 at 14.25.47.png

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

Turbo ID について

Turbo Frame Tagはデフォルトで dom_id のメソッドを使われるので、私たちが書いたコードを少しリファクタリングはできます:
例えば

<%# 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

また今度!

2
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
3