Webアプリケーションでページ遷移なしにデータを編集できる「インライン編集」は便利ですが、従来の実装はJavaScriptを多用し複雑になりがちでした。Ruby on RailsのHotwireに含まれるTurbo Framesは、この課題を解決します。サーバーサイドでHTMLを生成するRailsの利点を活かし、シンプルに動的なUIを実現する手法です。
この記事では、Turbo Framesを使ったインライン編集の仕組みと具体的な実装方法、注意点をコード例と共に解説します。
Turbo Framesによるインライン編集の要点と利点
Turbo Framesの基本動作
Turbo Framesは、HTMLの一部を<turbo-frame>
というカスタムHTMLタグと一意のid
属性で囲むことで、その領域を独立したコンテキストとして定義します。このフレーム内で発生したリンククリックやフォーム送信は、デフォルトではページ全体をリロードせず、そのフレーム自身のコンテンツのみを更新しようと試みます。
インライン編集への応用
この特性を利用し、データの「表示状態」と「編集フォーム状態」を、同じid
を持つ単一のTurbo Frame内でシームレスに切り替えます。
- 表示から編集へ: ユーザーが「編集」ボタンをクリックすると、Turboがサーバーにリクエストを送信。サーバーは同じフレーム
id
を持つ編集フォームHTMLを返却。Turboがフレームの内容を表示からフォームへ差し替えます。 - 編集から更新へ: ユーザーがフォームを送信すると、サーバーが更新処理を実行。成功なら同じフレーム
id
を持つ更新後の表示HTMLを、失敗(バリデーションエラー等)ならエラー情報付きの編集フォームHTMLを返却。Turboが再びフレーム内容を適切に差し替えます。
主な利点
- JSコード削減:
- フロントエンドの複雑な状態管理やDOM操作コードが不要になります。
- サーバーサイド中心の開発:
- Rails開発者が慣れ親しんだMVC、特にコントローラーとビュー(ERB)でのHTML生成に集中できます。
- 優れたユーザー体験 (UX):
- ページ遷移のない部分更新により、即時性が高くスムーズな操作感を提供します。
- 開発効率と保守性の向上:
- 実装がシンプルで直感的になり、コード量が減り、バックエンドにロジックが集約されるため、メンテナンスが容易になります。
実装の準備:コントローラーと一覧表示ビュー
インライン編集の実装に入る前に、まず商品データを取得するコントローラーのアクションと、そのデータを一覧表示する基本的なビューが必要です。
コントローラー (app/controllers/products_controller.rb
)
一覧表示のための index
アクションを用意します。
# app/controllers/products_controller.rb
class ProductsController < ApplicationController
before_action :set_product, only: [:edit, :update]
def index
@products = Product.all.order(created_at: :desc) # 商品リストを取得
end
# ... (edit, update アクションは後述) ...
private
def set_product
@product = Product.find(params[:id])
end
def product_params
params.require(:product).permit(:name, :price)
end
end
一覧表示ビュー (app/views/products/index.html.erb
)index
アクションに対応するビューで、@products
をループ処理し、各商品の表示を担当するパーシャルを呼び出します。
<%# app/views/products/index.html.erb %>
<div class="container mt-4">
<div class="d-flex justify-content-between align-items-center mb-3">
<h1>商品一覧</h1>
<%= link_to "新規商品登録", new_product_path, class: "btn btn-primary" %>
</div>
<div id="products">
<% if @products.any? %>
<% @products.each do |product| %>
<%# ↓ 各商品に対して "_product.html.erb" パーシャルをレンダリング %>
<%= render partial: "product", locals: { product: product } %>
<% end %>
<% else %>
<p>商品はまだ登録されていません。</p>
<% end %>
</div>
</div>
3. 実装ステップ:インライン編集の実装
一覧表示の準備ができたら、インライン編集の核となるパーシャルとアクションを実装します。
Step 1: 表示用パーシャル (_product.html.erb
)index.html.erb
から呼び出される、個々の商品の「表示状態」を定義します。
<%# app/views/products/_product.html.erb %>
<%# product オブジェクトを直接渡し、IDを自動生成 (例: id="product_1") %>
<%= turbo_frame_tag product do %>
<div class="card mb-3">
<div class="card-body">
<h5 class="card-title"><%= product.name %></h5>
<p class="card-text mb-2">価格: <%= number_to_currency(product.price) %></p>
<%= link_to "編集", edit_product_path(product), class: "btn btn-sm btn-outline-secondary" %>
</div>
</div>
<% end %>
turbo_frame_tag product
: 各商品を固有のIDを持つフレームで囲みます。- フレーム内に商品情報と「編集」リンク (
edit_product_path
) を含めます。
Step 2: 編集フォームビュー (edit.html.erb
)
「編集」リンクがクリックされた際に、対応するフレーム内に読み込まれるフォームです。エラー表示ロジックも含まれます。
<%# app/views/products/edit.html.erb %>
<%# ★最重要★ 表示用パーシャルと同じオブジェクト(@product)を渡し、同じIDを生成 %>
<%= turbo_frame_tag @product do %>
<div class="card mb-3">
<div class="card-body bg-light">
<%= form_with model: @product, url: product_path(@product), method: :patch do |f| %>
<%# 商品名入力フィールド %>
<div class="mb-3">
<%= f.label :name, class: "form-label" %>
<%= f.text_field :name, class: "form-control" %>
</div>
<%# 価格入力フィールド %>
<div class="mb-3">
<%= f.label :price, class: "form-label" %>
<div class="input-group">
<%= f.number_field :price, step: 0.01, class: "form-control" %>
<span class="input-group-text">円</span>
</div>
</div>
<%# アクションボタン %>
<div class="d-flex justify-content-end gap-2 mt-3">
<%= f.submit "更新する", class: "btn btn-primary btn-sm" %>
</div>
<% end %>
</div>
</div>
<% end %>
turbo_frame_tag @product
: 表示側と同じIDを生成します。form_with ... do |f|
: 更新処理を行うフォームです。
Step 3: コントローラーの実装 (edit
, update
アクション)
編集フォームの表示と更新処理を担当します。
# app/controllers/products_controller.rb
class ProductsController < ApplicationController
before_action :set_product, only: [:edit, :update]
def index
@products = Product.all.order(created_at: :desc)
end
def edit
# @product をビュー(edit.html.erb)に渡す
end
def update
if @product.update(product_params)
# === 更新成功 ===
respond_to do |format|
format.html { redirect_to @product, notice: '商品情報を更新しました。' }
format.turbo_stream {
render turbo_stream: turbo_stream.replace(
@product,
partial: "products/product",
locals: { product: @product }
)
}
end
else
# === 更新失敗 ===
# ★重要★ HTTPステータス 422 を付けて編集フォームを再レンダリング
render :edit, status: :unprocessable_entity
end
end
private
def set_product
@product = Product.find(params[:id])
end
def product_params
params.require(:product).permit(:name, :price)
end
end
edit
アクション: 編集対象の@product
を準備します。update
アクション: 更新処理を行い、成功時はturbo_stream.replace
で表示パーシャルを、失敗時はstatus: :unprocessable_entity
付きで編集フォームをレンダリングします。
4. 実装を成功させるための重要ポイント
- IDの一致は絶対:
- 表示/編集ビュー間で
turbo_frame_tag
のIDが一致しているか確認します。オブジェクトを直接渡す場合、同じオブジェクトを使うことが重要です。
- 表示/編集ビュー間で
- サーバー応答の適切性:
- 応答には対象フレームとその中身だけを含めます。
- エラーハンドリング:
- 更新失敗時にはコントローラーで
status: :unprocessable_entity
を返すことが重要です。ビューでのエラー表示と合わせて、ユーザーにフィードバックを提供します。
- 更新失敗時にはコントローラーで
- キャンセル機能の確認:
- キャンセルリンクが意図通り表示状態に戻るか確認します。
- Turbo Streamsとの使い分け:
- フレーム全体の置換以上の複雑なDOM操作にはTurbo Streamsを検討します。
- Stimulusによる機能強化:
- クライアント側の補助的な動作にはStimulusが適しています。
5. まとめ:Turbo Framesで次世代のRails開発を
RailsのTurbo Framesは、インライン編集のような一般的なUIパターンを、サーバーサイド中心の思想を保ちながら、驚くほどシンプルかつ効率的に実装するための強力な武器となります。
JavaScriptの複雑さから解放され、Railsの生産性をさらに高めるTurbo Frames。ぜひ、この技術をあなたのプロジェクトに取り入れ、より洗練されたユーザー体験と開発体験を実現してください。