はじめに
この記事は How to break out of a Turbo Frame and redirect? で紹介されている1つ目の解決策 turbo_page_requires_reload
について調べた記録です。
結論として、私は以下に記載する方法より Turbo.visit を利用して TurboStream カスタムアクションを追加する方法をおすすめします。こちらの方法も上記リンク先の記事で紹介されています。
課題
TurboFrame 内にあるフォームから POST された後、リダイレクトしたくても思うように動作しないことがあります。これはリダイレクトの GET リクエストにも TurboFrame Target の制約があるためです。
避けられればよいのですが、TurboFrame の中からのリクエストに対しページ全体をリフレッシュしたりページ遷移させたいことは、実際ときどきおこります。form タグに data-turbo-frame="_top"
をつけられればよいのですが、バリデーションエラーを表示したいときはこれができません。
解決策(の1つ)
リダイレクト先のページに turbo_page_requires_reload
を追加し、再び GET (HTML) させることで redirect_to に指定したページを表示させることができます。
turbo_page_requires_reload とは?
turbo-rails に用意されているヘルパーメソッドの1つです。
https://github.com/hotwired/turbo-rails/blob/0e42702df40698521cc7e506fea927bc3a2a08ab/app/helpers/turbo/drive_helper.rb#L42-L44
<meta name="turbo-visit-control" content="reload">
を <head>
に埋め込みます。
この turbo-visit-control reload の meta タグは turbo-frame からリクエストされたときにページリロードする効果があります。
https://turbo.hotwired.dev/reference/attributes#meta-tags
(注意)head タグの中に <%= yield :head %>
がなければ meta タグを埋め込むヘルパーメソッドが効かないので注意してください。この yield は turbo_frame/turbo_stream のレイアウトファイルや、Rails 8.0 の layouts/application.html.erb
には標準で用意されています。
サンプルコード(不具合あり)
model
class Memo < ApplicationRecord
# body:string だけの簡単なモデルです
validates :body, presence: true
end
controller
class MemosController < ApplicationController
before_action :set_memo, only: %i[ show edit update destroy ]
def index
@memos = Memo.order(id: :desc)
end
def new
@memo = Memo.new
end
def create
@memo = Memo.new(memo_params)
if @memo.save
redirect_to memos_path, status: :see_other, notice: "Memo was successfully created."
else
render :new, status: :unprocessable_entity
end
end
# (他のアクションは割愛)
private
# ...
def memo_params
params.expect(memo: [ :body ])
end
end
view
<div class="row mb-5">
<div class="col-12 col-md-6">
<%= turbo_frame_tag Memo.new, src: new_memo_path %>
</div>
</div>
<h2>Memos</h2>
<div class="row">
<table class="col-12 col-md-6 table table-striped">
<thead>
<tr>
<th>ID</th>
<th>Memo</th>
<th></th>
</tr>
</thead>
<tbody>
<% @memos.each do |memo| %>
<tr>
<td><%= memo.id %></td>
<td>
<%= turbo_frame_tag memo, src: memo_path(memo), loading: :lazy %>
<td>
<%= button_to "Destroy", memo, method: :delete, class: "btn btn-sm btn-danger", data: { turbo_confirm: "Are you sure?" } %>
</td>
</tr>
<% end %>
</tbody>
</table>
</div>
<%= turbo_frame_tag @memo do %>
<h2>New memo</h2>
<%= render "form", memo: @memo %>
<% end %>
<%= form_with(model: memo) do |form| %>
<% #... %>
<div class="mb-3">
<%= form.text_field :body, class: "form-control" %>
</div>
<div class="d-flex gap-3 align-items-center">
<%= form.submit class: "btn btn-primary" %>
<% if memo.persisted? %>
<%= link_to "Cancel", memo_path(memo) %>
<% end %>
</div>
<% end %>
これは、このままだと create アクションが正しく動作しません。
リクエスト 1回目 POST
リクエスト 2回目 GET (303 redirect + TurboFrame/TurboStream)
となりますが、TurboFrame のみを書き換えようとし、結果、TurboFrame の中身が消えてしまいます。
修正コード
<% turbo_page_requires_reload %>
<% #... %>
turbo_page_requires_reload
を index view に追加し、<meta name="turbo-visit-control" content="reload">
を head に差し込みます。
そうすると
リクエスト 1回目 POST
リクエスト 2回目 GET (303 redirect + TurboFrame/TurboStream)
リクエスト 3回目 GET (HTML) ... turbo-visit-control により reload される
となり、index ページ全体がリフレッシュされます。
ただし、flash メッセージが redirect による GET で消えてしまうため、flash.keep
をしておく必要があります。
def index
flash.keep if turbo_frame_request?
# ...
end
(疑問)以下のソースをみると before_action に同様のコードがあるため、追加する必要はないように思えるが、実際、処理を追加しないと動かなかった
https://github.com/hotwired/turbo-rails/blob/0e42702df40698521cc7e506fea927bc3a2a08ab/app/controllers/turbo/frames/frame_request.rb#L26
おわりに(感想など)
上記の方法はすごく少ない修正で解決できるのがメリットですが、同じデータを2回 GET しており、表示するコストが大きいページでこの方法を選択するのは避けたいです。
「はじめに」で書いたように Turbo.visit を実行する TurboStream のカスタムアクションを用意するのが汎用的でよい方法だと思いますし、同じページをリフレッシュするなら turbo_stream.refresh という選択肢もあります。
flash.notice = "Memo was successfully created."
render turbo_stream: turbo_stream.refresh(request_id: nil)
※ request_id: nil
についてはこちらの issue を参照
データ登録後に「完了しました」ページや「これ以上は利用できません」などのエラーページに遷移させたいくらいの用途でしたらマッチするかもしれませんが、Turbo.visit できるカスタムアクションがあればこの方法は選択することはないと思います。
今回は turbo_page_requires_reload という機能がある、ということだけ頭の片隅にしまっておきます。
告知
Hotwire.love という勉強会を毎月第3 or 第4木曜に開催しています。この記事も Hotwire.love の中でとりあげたものを調べてまとめてみました。hotwire に興味のあるかたは是非遊びに来てください。