mcpField
T
turbo

turbo

github.com/thinkoodle開発7190 回インストール8 週間前に更新
リポジトリ

turbo について

Turbo Drive、Frames、Streamsを使い、独自JavaScriptを抑えて画面を構築するRails開発者向けスキルです。部分更新やリアルタイム配信、フォーム、Turbo 8のMorphingを扱います。

主な機能

  1. 01Turbo Driveのナビゲーションと無効化
  2. 02Turbo Framesによる部分更新
  3. 03Turbo StreamsによるDOM更新
  4. 04ActionCableのリアルタイム配信
  5. 05Turbo 8のページMorphing
  6. 06Turboフォームとバリデーション処理

活用シーン

  1. 01Railsページに遅延読み込みを追加
  2. 02インライン編集、タブ、モーダルの構築
  3. 03ActionCableでリアルタイム更新を配信
  4. 04Turboフォームの検証エラーを処理

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/thinkoodle/rails-skills --skill turbo

インストール後、Claude Code で/turboを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

references/
broadcasting.md
edge-cases.md
frames.md
morphing.md
streams.md
testing.md

詳細

Rails Turbo エキスパート

Turbo Drive、Frames、Streams を使って、カスタム JavaScript を使わずに(または最小限に抑えて)高速でモダンな Rails UI を構築します。

フィロソフィー

基本原則:

  1. ワイヤー越しの HTML — サーバーが HTML をレンダリングし、Turbo がそれを届けます。UI に JSON API は使いません。
  2. プログレッシブエンハンスメント — Turbo Drive は自動で動作します。Frames と Streams はその上に重ねて使います。
  3. サーバーが信頼できる唯一の情報源 — ビジネスロジックは Ruby に置きます。Turbo は HTML をやり取りするだけです。
  4. 最小限の JavaScript — DOM を更新するために JS を書いているなら、おそらくやり方が間違っています。Turbo Streams を使いましょう。
  5. 組み合わせ可能なプリミティブ — Drive、Frames、Streams はそれぞれ1つの課題を解決します。組み合わせて使いましょう。

メンタルモデル:

Turbo Drive  →  Speeds up ALL navigation (automatic, zero config)
Turbo Frames →  Scopes updates to a REGION of the page (explicit, per-element)
Turbo Streams → Delivers TARGETED mutations to ANY element (server-pushed or response)

判断フロー:

Need faster page loads?                    → Turbo Drive (already on)
Need to update PART of a page on click?    → Turbo Frame
Need to update MULTIPLE parts at once?     → Turbo Stream
Need real-time updates from other users?   → Turbo Stream + ActionCable broadcast
Need smooth page refresh without flicker?  → Turbo Morph (Rails 8+)

このスキルを使う場面

  • ナビゲーションや更新の範囲を限定するために turbo_frame_tag を追加する
  • コントローラーのアクションから turbo_stream レスポンスを返す
  • ActionCable を使ってリアルタイムのブロードキャストを設定する
  • 遅延読み込みコンテンツを実装する
  • インライン編集、タブ付きインターフェース、モーダルを構築する
  • Turbo Drive の動作を設定する(特定のリンクやフォームで無効にする)
  • Turbo 8 のモーフィングとページ更新を使う

手順

ステップ 1:既存の設定を確認する

Turbo Drive は Rails 7 以降のすべてのアプリでデフォルトで有効です。インストールも設定も不要です。すべてのリンクのクリックとフォームの送信は、すでに Turbo Drive を通ります。

アプリに turbo-rails があるか確認します:

# Should already be in Gemfile
grep "turbo-rails" Gemfile

# JavaScript import should exist
rg "import.*turbo" app/javascript/

存在しない場合(Rails 7 以降ではまれです):

bundle add turbo-rails
bin/rails turbo:install
ステップ 2:Turbo Drive — 無効にするタイミングを把握する

Turbo Drive はすべての <a> のクリックと <form> の送信を捕捉し、AJAX で取得して <body> を置き換えます。これは自動で行われます。必要なのは無効にする場合だけです。

<%# Disable for a specific link or form %>
<%= link_to "External Site", "https://example.com", data: { turbo: false } %>
<%= form_with model: @upload, data: { turbo: false } do |f| %>

<%# Disable for a whole section %>
<div data-turbo="false">...</div>

よくある無効化のケース: Active Storage のダイレクトアップロードを使わないファイルアップロード、外部リンク、OAuth/SSO のリダイレクト、ファイルのダウンロード、決済フォーム(Stripe)。

GET 以外のリンクメソッド — turbo_method を付けた link_to より button_to を優先します:

<%# Acceptable %>
<%= link_to "Delete", post_path(@post), data: { turbo_method: :delete, turbo_confirm: "Sure?" } %>

<%# Better — more accessible and semantic %>
<%= button_to "Delete", post_path(@post), method: :delete,
    form: { data: { turbo_confirm: "Are you sure?" } } %>
ステップ 3:Turbo Frames — ページの一部を更新する

Turbo Frames は中核となる機能です。ページの領域を指定してナビゲーションを制御します。フレーム内のリンクやフォームを操作すると、そのフレームだけが更新されます。

⚠️ エージェントが最もよく間違える点:対応する turbo_frame_tag で両側を囲むのを忘れること。

現在のページとレスポンスページの両方に、同じ ID の <turbo-frame> が必要です。一致しない場合、Content Missing エラーが発生します。

基本的な Frame パターン:

<%# index.html.erb — the page with the frame %>
<%= turbo_frame_tag "post_#{@post.id}" do %>
  <h2><%= @post.title %></h2>
  <%= link_to "Edit", edit_post_path(@post) %>
<% end %>

<%# edit.html.erb — the response MUST have matching frame %>
<%= turbo_frame_tag "post_#{@post.id}" do %>
  <%= render "form", post: @post %>
<% end %>

一貫した ID には dom_id を使います:

<%= turbo_frame_tag dom_id(@post) do %>
  <%# dom_id(@post) produces "post_123" %>
<% end %>

遅延読み込みフレーム(ページのレンダリング後にコンテンツを読み込む):

<%= turbo_frame_tag "comments", src: post_comments_path(@post), loading: :lazy do %>
  <p>Loading comments...</p>
<% end %>

src URL は自動で取得されます。loading: :lazy を指定すると、フレームが表示されるまで読み込みを遅らせます(Intersection Observer)。:lazy を指定しない場合は、ページの読み込み後すぐに読み込まれます。

⚠️ エージェントが見落としやすい点:遅延読み込みのエンドポイントは、対応する turbo_frame_tag を含むページを返す必要があります。 コントローラー側でフレームかどうかを把握する必要はありません。通常どおりレンダリングすると、Turbo が一致するフレームを抽出します。

フレームから抜けてページ全体を対象にする:

<%= turbo_frame_tag dom_id(@post) do %>
  <%# This link stays in the frame %>
  <%= link_to "Edit", edit_post_path(@post) %>
  
  <%# This link breaks out and navigates the full page %>
  <%= link_to "Show Full Page", post_path(@post), data: { turbo_frame: "_top" } %>
<% end %>

別のフレームを対象にする:

<%= turbo_frame_tag "sidebar" do %>
  <%= link_to "Load Details", post_path(@post), data: { turbo_frame: "main_content" } %>
<% end %>

<%# This frame will be updated instead %>
<%= turbo_frame_tag "main_content" do %>
  <p>Select a post from the sidebar</p>
<% end %>

カスタムの target 属性を持つフレーム(フレーム内のすべてのリンクが別のフレームを対象にする):

<%= turbo_frame_tag "nav", target: "main_content" do %>
  <%# ALL links here update "main_content" frame instead %>
  <%= link_to "Posts", posts_path %>
  <%= link_to "Users", users_path %>
<% end %>
ステップ 4:Turbo Streams — DOM の対象を絞った変更

Turbo Streams はピンポイントです。「この HTML を指定した DOM 要素に追加/置換/削除する」と指示します。次の 2 つのコンテキストで動作します。

  1. HTTP レスポンス — フォーム送信(POST/PUT/PATCH/DELETE)から返されます
  2. WebSocket ブロードキャスト — ActionCable 経由ですべての接続ユーザーにプッシュされます

7 つのストリームアクション:

アクション動作
append対象要素の子要素の末尾に HTML を追加します
prepend対象要素の子要素の先頭に HTML を追加します
replace対象要素全体を置き換えます
update対象要素の内部 HTML を置き換えます
remove対象要素を削除します
before対象要素の前に HTML を挿入します
after対象要素の後に HTML を挿入します

⚠️ 重要な違い:replace と update

  • replace — 対象要素自体を削除し、新しい HTML で置き換えます
  • update — 対象要素はそのまま残し、子要素を置き換えます
<%# replace: <div id="post_1"> is GONE, replaced entirely %>
<%= turbo_stream.replace dom_id(@post), partial: "posts/post", locals: { post: @post } %>

<%# update: <div id="post_1"> stays, its contents are swapped %>
<%= turbo_stream.update dom_id(@post), partial: "posts/post", locals: { post: @post } %>

コントローラーから Turbo Streams で応答する:

# app/controllers/posts_controller.rb
def create
  @post = Post.new(post_params)

  respond_to do |format|
    if @post.save
      format.turbo_stream  # Renders create.turbo_stream.erb
      format.html { redirect_to @post }
    else
      format.html { render :new, status: :unprocessable_entity }
    end
  end
end
<%# app/views/posts/create.turbo_stream.erb %>
<%= turbo_stream.prepend "posts", partial: "posts/post", locals: { post: @post } %>
<%= turbo_stream.update "post_count", html: "#{Post.count} posts" %>
<%= turbo_stream.update "new_post_form" do %>
  <%= render "form", post: Post.new %>
<% end %>

⚠️ エージェント向けの注意点:Turbo Stream のレスポンスは GET 以外のリクエストでのみ動作します。 GET リクエストでは Turbo Drive または Frames を使います。GET に対して turbo_stream 形式を返そうとしても、動作しません。代わりにフレームを使ってください。

インラインでストリームをレンダリングする(テンプレートを省略):

def destroy
  @post.destroy

  respond_to do |format|
    format.turbo_stream { render turbo_stream: turbo_stream.remove(dom_id(@post)) }
    format.html { redirect_to posts_path }
  end
end

複数のストリームアクションをインラインで指定する:

format.turbo_stream do
  render turbo_stream: [
    turbo_stream.remove(dom_id(@post)),
    turbo_stream.update("post_count", html: "#{Post.count} posts")
  ]
end
ステップ 5:Turbo Stream のブロードキャスト(リアルタイム)

ブロードキャストは、ActionCable 経由でチャンネルを購読しているすべてのユーザーに Turbo Stream アクションをプッシュします。これにより、JavaScript を書かずに「リアルタイム」機能を実現できます。

ビューで購読を設定する:

<%# This creates an ActionCable subscription %>
<%= turbo_stream_from "posts" %>

<%# Scoped to a specific record %>
<%= turbo_stream_from @project %>

<%# Multiple stream names %>
<%= turbo_stream_from @project, "messages" %>

モデルからブロードキャストする:

class Post < ApplicationRecord
  # Broadcast AFTER commit (not after save — important for transactions)
  after_create_commit  { broadcast_append_to "posts" }
  after_update_commit  { broadcast_replace_to "posts" }
  after_destroy_commit { broadcast_remove_to "posts" }

  # Shorthand for all three:
  broadcasts_to ->(post) { "posts" }
  # Or if broadcasting to a parent:
  broadcasts_to :project
end

カスタムの partial を使ってブロードキャストする:

after_create_commit do
  broadcast_append_to "posts",
    target: "posts_list",
    partial: "posts/post_card",
    locals: { post: self }
end

任意の場所(コントローラー、ジョブ、サービス)からブロードキャストする:

Turbo::StreamsChannel.broadcast_append_to("posts", target: "posts_list",
  partial: "posts/post", locals: { post: @post })
Turbo::StreamsChannel.broadcast_remove_to("posts", target: dom_id(@post))

⚠️ エージェント向けの注意点:ブロードキャストはリクエストコンテキストなしで partial をレンダリングします。 そのため、current_user、request、セッションは利用できません。これらがなくても動作するように partial を設計するか、必要なデータを locals として渡してください。

ステップ 6:Turbo 8 のモーフィング(Rails 8+)

モーフィングは、置き換えではなく差分比較によって DOM を更新するページ更新戦略です。フォームの状態、スクロール位置、CSS トランジションが保持されます。

ページでモーフィングを有効にする:

<%# In the <head> of your layout or page %>
<%= turbo_refreshes_with method: :morph, scroll: :preserve %>

この設定の動作:

  • method: :morph — 新しい HTML と現在の DOM を比較し、最小限の変更を適用します
  • scroll: :preserve — 更新後もスクロール位置を保持します

サーバーからモーフィング更新を実行する:

# In a broadcast:
after_update_commit do
  broadcast_refresh_to "posts"
end

# Shorthand:
broadcasts_refreshes

モーフィングとストリームの使い分け:

  • モーフィング — 状態(フォーム、スクロール)を維持しながらページ全体を再レンダリングしたい場合
  • ストリーム — 特定の要素を対象に、ピンポイントで更新したい場合

モーフィングの間、保持する要素を指定する:

<div id="player" data-turbo-permanent>
  <%# This element survives morph refreshes intact %>
</div>
ステップ 7:Turbo でのフォーム

フォームは、エージェントが Turbo で最もつまずきやすい箇所です。重要なルールは次のとおりです。

ルール 1:バリデーションに失敗した場合は、必ず 422 Unprocessable Entity を返します。

def create
  @post = Post.new(post_params)
  if @post.save
    redirect_to @post
  else
    render :new, status: :unprocessable_entity  # ← CRITICAL
  end
end

:unprocessable_entity がないと、Turbo はエラーレスポンスをレンダリングしません。代わりにリダイレクトのステータスに従います。

ルール 2:フレーム内のフォームはフレーム内にとどまります。

<%= turbo_frame_tag "new_post" do %>
  <%= form_with model: Post.new do |f| %>
    <%= f.text_field :title %>
    <%= f.submit "Create" %>
  <% end %>
<% end %>

フォームは Turbo 経由で送信されます。レスポンスには、一致する turbo_frame_tag "new_post" が含まれている必要があります。または、コントローラーが turbo_stream 形式で応答すれば、フレームの要件を省略できます。

ルール 3:フォーム送信が成功したらリダイレクトします。

# Turbo handles redirects correctly — it navigates the page
if @post.save
  redirect_to @post, notice: "Created!"  # Works fine with Turbo
end

ルール 4:Rails 7 以降では、form_with はデフォルトで Turbo を使います。 local: false は不要です。無効にするには:

<%= form_with model: @post, data: { turbo: false } do |f| %>
ステップ 8:一般的なパターン

詳細な実装については、references/ ディレクトリを参照してください。

  • references/frames.md — インライン編集、タブナビゲーション、遅延読み込み、モーダル、無限スクロール
  • references/streams.md — フラッシュメッセージ、ライブ検索、ネストされたフォーム、カウンター、トースト、テンプレートの規約
  • references/broadcasting.md — スコープ付きブロードキャスト、ユーザー固有のストリーム、バックグラウンドジョブのパターン
  • references/morphing.md — Turbo 8 のモーフ更新のセットアップ、モーフとストリームの使い分け
  • references/testing.md — インテグレーションテスト、システムテスト、ブロードキャストのアサーション
  • references/edge-cases.md — Stimulus との連携、注意点(ファイルアップロード、DELETE リダイレクト、CSP、キャッシュ)

クイックリファレンス

ヘルパーメソッド
<%# Frames %>
<%= turbo_frame_tag "id" %>
<%= turbo_frame_tag dom_id(@record) %>
<%= turbo_frame_tag "id", src: path, loading: :lazy %>
<%= turbo_frame_tag "id", target: "_top" %>

<%# Stream subscription %>
<%= turbo_stream_from "channel_name" %>
<%= turbo_stream_from @record %>

<%# Stream actions (in .turbo_stream.erb templates) %>
<%= turbo_stream.append "target_id", partial: "partial" %>
<%= turbo_stream.prepend "target_id", partial: "partial" %>
<%= turbo_stream.replace dom_id(@record), partial: "partial" %>
<%= turbo_stream.update "target_id", html: "content" %>
<%= turbo_stream.remove dom_id(@record) %>
<%= turbo_stream.before dom_id(@record), partial: "partial" %>
<%= turbo_stream.after dom_id(@record), partial: "partial" %>

<%# Morphing (Rails 8+) %>
<%= turbo_refreshes_with method: :morph, scroll: :preserve %>
データ属性
data-turbo="false"              <%# Disable Turbo for this element %>
data-turbo-method="delete"      <%# HTTP method for link %>
data-turbo-confirm="Sure?"      <%# Confirmation dialog %>
data-turbo-frame="_top"         <%# Break out of frame %>
data-turbo-frame="frame_id"     <%# Target specific frame %>
data-turbo-permanent             <%# Preserve across morphs %>
data-turbo-temporary             <%# Remove on morph refresh %>
data-turbo-action="advance"     <%# Push to browser history %>
data-turbo-action="replace"     <%# Replace browser history entry %>
モデルのブロードキャストメソッド
# Individual callbacks
after_create_commit  { broadcast_append_to "stream" }
after_update_commit  { broadcast_replace_to "stream" }
after_destroy_commit { broadcast_remove_to "stream" }

# All-in-one
broadcasts_to ->(record) { "stream_name" }
broadcasts_to :parent_association

# Morphing (Rails 8+)
broadcasts_refreshes
after_update_commit { broadcast_refresh_to "stream" }

# Manual broadcast from anywhere
Turbo::StreamsChannel.broadcast_append_to("stream", target: "id", partial: "path")
Turbo::StreamsChannel.broadcast_remove_to("stream", target: "id")
Turbo::StreamsChannel.broadcast_refresh_to("stream")
コントローラーのレスポンスパターン
respond_to do |format|
  if @record.save
    format.turbo_stream  # renders action.turbo_stream.erb
    format.html { redirect_to @record }
  else
    format.html { render :new, status: :unprocessable_entity }
  end
end

エージェントがよく起こす間違い

  1. 一致するフレーム ID がない — 両方のページで同じ ID の turbo_frame_tag が必要です
  2. バリデーション失敗時に 200 を返す — status: :unprocessable_entity(422)を指定する必要があります
  3. GET リクエストで Turbo Stream を使う — ストリームが機能するのは POST/PUT/PATCH/DELETE のみです
  4. ブロードキャストの部分テンプレートで current_user を使う — ブロードキャストにはリクエストコンテキストがありません
  5. ビューで turbo_stream_from を忘れる — ブロードキャストにはサブスクリプションが必要です
  6. update のつもりで replace を使う — replace はターゲット要素全体を削除します
  7. dom_id を使わない — 手動で設定した ID はずれていきますが、dom_id(@post) は一貫しています
  8. after_*_commit を忘れる — ブロードキャストには after_save ではなくコミットコールバックを使います
  9. respond_to に HTML のフォールバックがない — Turbo を使わないクライアント向けに、必ず format.html を含めてください
  10. プレースホルダーコンテンツのない遅延フレーム — フレームタグ内に読み込み状態を表示してください

アンチパターン

  1. UI 更新のためだけに JSON API を構築する — 代わりに Turbo Streams を使いましょう
  2. DOM コンテンツを入れ替える JavaScript を書く — そのために Turbo があります
  3. フレームを深くネストする — 1〜2 階層に抑えてください。複雑さが急激に増大します
  4. モデルの変更をすべてブロードキャストする — 選択的に行ってください。ブロードキャストが多すぎるとパフォーマンスの問題につながります
  5. 巨大な turbo_stream.erb テンプレート — ストリームのレスポンスは目的を絞り、1 つのレスポンスにつき 1〜3 個のアクションに抑えてください
  6. ストリームが必要な用途に Turbo Frames を使う — 関連のない複数の領域を更新する必要がある場合は、Streams を使ってください
  7. HTML のフォールバックを省略する — アプリは Turbo がなくても動作する必要があります(プログレッシブエンハンスメント)

コメント