16
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ソニックガーデン プログラマAdvent Calendar 2024

Day 2

turbo_page_requires_reload を使って TurboFrame の中からリダイレクトしてみた

Last updated at Posted at 2024-12-01

はじめに

この記事は 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
app/models/memo.rb
class Memo < ApplicationRecord
  # body:string だけの簡単なモデルです
  validates :body, presence: true
end
controller
app/controllers/memos_controller.rb
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
app/views/memos/index.html.erb
<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>
app/views/memos/new.html.erb
<%= turbo_frame_tag @memo do %>
  <h2>New memo</h2>
  <%= render "form", memo: @memo %>
<% end %>
app/views/memos/_form.html.erb
<%= 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 の中身が消えてしまいます。

修正コード

app/views/memos/index.html.erb
<% 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 をしておく必要があります。

app/controllers/memos_controller.rb
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 に興味のあるかたは是非遊びに来てください。

16
1
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
16
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?