モーダル
モーダルの中からformを送信し、送信中はpending UIを表示します。また成功したらトーストを表示し、表示中のデータを更新し、さらにモーダルが自動的に閉じるようにします。

formの送信はTurboで行います。レスポンスを返す方法としてはTurbo FramesとTurbo Streamsが考えられますが、今回はredirectせずにトーストを出したりもしたいため、画面の一箇所しか書き換えられないTurbo Framesではなく、Turbo Streamsを選択しますform送信が成功した場合に限り、モーダルを閉じます
form送信結果を受け取り、JavaScriptを起動する必要があります。これはStimulusで行いますform送信が失敗した場合の処理は別途解説します<%= form_with( model: todo, id: 'todo-form', data: { action: "turbo:submit-end->modal-dialog#hideOnSuccess" }, ) do |form| %> <% if todo.errors.any? %> <div class="text-red-600"> <ul> <% todo.errors.each do |error| %> <li><%= error.full_message %></li> <% end %> </ul> </div> <% end %> <div> <%= form.label :title, class: "block text-sm/6 font-semibold text-gray-900" %> <div class="mt-2.5"> <%= form.text_field :title, data: {action: "keydown.enter->modal-dialog#void:prevent"}, class: "block w-full rounded-md border-0 px-3.5 py-2 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-orange-600 sm:text-sm/6"%> </div> </div> <div class="mt-8 flex justify-between"> <button type="button" class="btn-outline-primary" data-action="click->modal-dialog#hide"> キャンセル </button> <%= form.submit class: "btn-primary" %> </div> <% end %>
<form>の箇所ですdata: { action: "turbo:submit-end->modal-dialog#hideOnSuccess" }のところは、<form>の送信後にStimulus ModalDialogControllerのhideOnSuccess()メソッドを呼ぶという指示です
<form>送信後はturbo:submit-endを発火しますので、これを利用しますturbo:submit-endは失敗した時も発火しますので、hideOnSuccess()メソッドではレスポンスがsuccessであったことを確認した上でモーダルを隠す処理をしますfetchをawaitしてリクエスト送信後の処理を記述します。それに対してHotwireはこのようにイベント中心の記述をします<button>(キャンセル)のところは、クリックしたらModalDialogControllerのhide()を呼ぶようにしていますclass TodosController < ApplicationController # ... def update respond_to do |format| if @todo.update(todo_params) flash.now.notice = "Todo was successfully updated." format.turbo_stream else format.turbo_stream { render status: :unprocessable_content} end end end # ... private # ... # Only allow a list of trusted parameters through. def todo_params params.require(:todo).permit(:title) end end
<% if @todo.errors.any? %> <%= turbo_stream.replace "todo-form" do %> <%= render "form", todo: @todo %> <% end %> <% else %> <%= turbo_stream.replace dom_id(@todo) do %> <%= render @todo, highlight: true %> <% end %> <%= turbo_stream.replace "global-notification" do %> <%= render "global_notification" %> <% end %> <% end %>
updateのActionでリクエストを受け取り、レスポンスを返しますflash.nowにトーストのメッセージをセットし、Turbo Streamのレスポンスを返しています
flashを使うところを、ここではflash.nowを使っています。flashはリダイレクト後にトーストを表示するときに使いますので、「次回のリクエスト」に内容を表示するためのものですflash.nowは「現在のリクエスト」を対象にしますflashを、今回のようにPOSTに対して直接レスポンスを返している場合はflash.nowを使い分ける形になりますif ... elseのelseの方だけみます
turbo_stream.replace dom_id(@todo)でデータが更新された行をreplaceで置換していますturbo_stream.replace "global-notification"ではトーストを表示する箇所を指定しています。そこにglobal_notification partialを挿入しています。このpartialはflashの内容に基づいて、トーストを表示しますimport {Controller} from "@hotwired/stimulus" // Connects to data-controller="modal-dialog" export default class extends Controller { static values = { shown: {type: Boolean, default: false}, page: String } // ... hide(event) { this.shownValue = false } // ... hideOnSuccess(event) { if (!event.detail.success) { return } this.hide(event) } // ... }
turbo:submit-endイベントに対してhideOnSuccess(event)メソッドを実行させるように<form>のdata-action属性を記述しましたhideOnSuccess(event)メソッドはステータスがsuccessだったかどうかを確認し、そうだった場合はhide(event)メソッドを呼び出してモーダルを表示します
turbo:submit-endのイベントを検知して、Stimulus controllerの中で行いました。アニメーション付きでモーダルを閉じました解説は長くなりましたが、