Putting modal dialog framework into Rails 7 application. Using this approach by David Colby. But not going as far as it does with dispatch events. As the article notes need to be on Turbo 7.2 at least.
We will create modal_controller.js in app/javascript/controllers
. Very similar to excid3 tailwind stimulus components modal component.
// This controller is an edited-to-the-essentials version of the modal component created by @excid3 as part of the essential tailwind-stimulus-components package found here:
// https://github.com/excid3/tailwindcss-stimulus-components
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ['container'];
connect() {
this.toggleClass = 'hidden';
this.backgroundId = 'modal-background';
this.backgroundHtml = this._backgroundHTML();
}
disconnect() {
this.close();
}
open() {
document.body.classList.add('fixed', 'inset-x-0', 'overflow-hidden');
this.containerTarget.classList.remove(this.toggleClass);
document.body.insertAdjacentHTML('beforeend', this.backgroundHtml);
this.background = document.querySelector(`#${this.backgroundId}`);
}
close() {
if (typeof event !== 'undefined') {
event.preventDefault()
}
this.containerTarget.classList.add(this.toggleClass);
if (this.background) { this.background.remove() }
}
_backgroundHTML() {
return `<div id="${this.backgroundId}" class="fixed top-0 left-0 w-full h-full" style="background-color: rgba(0, 0, 0, 0.7); z-index: 9998;"></div>`;
}
}
The register it in app/javascript/controllers\index.js
import ModalController from "./modal_controller"
application.register("modal", ModalController)
In app/views/shared add _modal.html.erb
<div data-modal-target="container"
class="hidden fixed inset-0 overflow-y-auto flex items-center justify-center"
style="z-index: 9999;">
<div class="max-w-2xl max-h-screen w-full relative">
<div class="m-1 bg-white rounded shadow">
<div class="px-4 py-5 border-b border-gray-200 sm:px-6">
<h3 id="modal-title" class="text-lg leading-6 font-medium text-gray-900"></h3>
</div>
<div id="modal-body"></div>
</div>
</div>
</div>
Update application template app/views/layouts/application.html.erb
link to the modal data controller and render the modal (initially it will be hidden>
<body class="h-full" data-controller="modal">
<!-- REST OF YOUR BODY <%= yeild %> etc. -->
<%= render "shared/modal" %>
</body>
Most common scenario is to say edit an item in modal so first change link_to for your edit to this
link_to edit_event_path(event), class:"group flex", data: { action: "click->modal#open", turbo_stream: "" }
Then create turbo_stream file to handle response touch app/views/events/edit.turbo_stream.erb
in your views folder
In the turbo_stream.erb add the lines to render the form (pretty much like edit.html) in modal
<%= turbo_stream.update "modal-title", "Edit Event" %>
<%= turbo_stream.update "modal-body", partial: "events/form", locals: { event: @event } %>
In this scenario the normal controller action on the successful save is fine, it will refresh index. In the event of error need to update the failed path so for example the edit action might look like this with the additional turbo_stream to referesh modal when there is an error
def update
respond_to do |format|
if @event.update(event_params)
format.html { redirect_to events_path, notice: "Event was successfully updated." }
format.json { render :show, status: :ok, location: @event }
else
format.html { render :edit, status: :unprocessable_entity }
format.json { render json: @event.errors, status: :unprocessable_entity }
# add this line...
format.turbo_stream { render turbo_stream: turbo_stream.replace('event-form', partial: 'form', locals: {event: @event}), status: :unprocessable_entity }
end
end
end
In order for this to work small change needed to the _form.html in the views folder add id e.g. <%= form_with(model: event, id: "event-form") do |form| %>
the name corresponding to the replace we had in the controller above.
Probably need Cancel/Close button on theform here is need to add data attribute to the link to close model e.g.
link_to "Close", "#", data: {action: "click->modal#close"}