Turbo
Modal forms
One shared modal any link can load a form into, and a responder that saves and repaints the page once.
One <dialog> in the layout, around a turbo frame. A link aimed at the frame loads a page's
form into it, and the same action renders a full page when it's visited directly, so there's no second
template for the modal.
1. Mount the modal
Render it once, in the layout:
<%= modal_frame %>The dialog opens as soon as the frame starts fetching, with a skeleton until the response lands. A failed load shows an error with a retry. Escape, a click on the backdrop or the close button closes it.
2. Link to a form
modal_link_to takes link_to's arguments. side: :start or
:end opens it as a drawer.
<%= modal_link_to "Edit", edit_label_path(label), class: button_classes %>3. Render the form in a dialog
dialog form: takes form_with's options and builds the form around the whole panel,
so a submit in the footer is inside it. When the request is for the modal frame, the dialog wraps itself in
that frame; otherwise it renders as it is.
<%# app/views/labels/edit.html.erb %>
<%= dialog title: "Edit label", form: { model: @label } do |dialog, form| %>
<%= form.field :name, "Name" %>
<% dialog.footer { form.submit } %>
<% end %>4. Respond from the controller
DialogResponder answers a save with turbo_stream.refresh, which morphs the page
behind the modal. The modal closes in that same render, so the page repaints once, straight to the new
state. A request without Turbo gets a plain redirect. A failed save renders the form again with
:unprocessable_content, and the dialog stays open showing the errors.
class LabelsController < ApplicationController
include Unmagic::Components::DialogResponder
def update
if @label.update(label_params)
refresh_or_redirect labels_path, notice: "Label saved."
else
render :edit, status: :unprocessable_content
end
end
end
The refresh needs turbo_refresh_method_tag :morph in the layout to keep scroll position and
state. A plain redirect_to works too: the modal visits the page it lands on and closes. A
response that renders into the frame, such as a wizard's next step, keeps the dialog open.
The frame's id is "modal". Change it with config.modal_frame_id. Needs
import "unmagic/components/modal".