Modals With Hotwire And Stimulus
When building web applications, often times you would be required to create a pop-up modal. Most use cases would be a pop-up form, or an alert message after a certain action. With the introduction of Hotwire and Stimulus on Rails 7, the old ways of using remote: true
and with a js.erb
file to render dynamic content within those modals won’t work. This would tackle how to implement pop-up modals using Hotwire and Stimulus.
Introducing Turbo
First thing you’re going to need to utilize is turbo-frame
. Turbo frames are predefined parts of a page that can be updated on request. Based on that, we would want to create a turbo-frame
for our modal.
# layouts/application.html.erb
...
<turbo-frame id="modal"></turbo-frame>
...
If you would want to be able to easily render a modal anywhere from your application, it would be best to add it inside the application.html.erb
. This would allow any page to have a predefined part for your modal if ever you would want to use it.
Creating a Reusable Modal Partial
Next thing would be creating a modal partial that we can reuse within our application. For this example I would be using bootstrap
for CSS.
<%= turbo_frame_tag 'modal' do %>
<div class="modal fade" tabindex="-1" role="dialog" aria-hidden="true"
data-controller="modal" data-modal-target="modal">
<div class="modal-dialog modal-dialog-centered modal-xl">
<div class="modal-content">
<div class="modal-header position-relative d-flex justify-content-between">
<h5 class="modal-title font-18"><%= title %></h5>
<button type="button" class="btn" data-action="modal#close" aria-label="Close">
X
</button>
</div>
<div class="modal-body">
<div class="container-fluid">
<%= yield %>
</div>
</div>
</div>
</div>
</div>
<% end %>
The partial is wrapped inside a turbo_frame_tag
called modal
. The same id we assigned to the turbo-frame
above. This is intentional because this would replace the predefined turbo-frame
we added in the application.html.erb
.
Then it would accept a title
and it would render the block given to this partial inside the modal body.
Creating a Stimulus Controller
From the above snippet for the partial, you would notice the lines data-controller="modal"
and data-action="modal#close"
. We would want to use a Stimulus Controller for the following reasons:
1. Opening the Modal
2. Closing the Modal
The lines below are implemented with Bootstrap Modal, may differ in case you are using a different CSS Framework.
# app/javascripts/modal_controller.js
import { Controller } from "@hotwired/stimulus";
export default class extends Controller {
static targets = [ "modal" ]
connect() {
this.modalTarget.classList.add('modal-open')
this.modalTarget.classList.add('show')
this.modalTarget.style.display = 'block'
this.modalTarget.removeAttribute('aria-hidden')
this.modalTarget.setAttribute('aria-modal', true)
this.modalTarget.setAttribute('role', 'dialog')
}
close() {
this.modalTarget.remove()
}
}
Note: In case you are not automatically importing all your stimulus controllers, you might need to add this line inside your index.js
file.
import ModalController from "./modal_controller"
application.register("modal", ModalController)
Every time the partial is called e.g. render 'layous/modal'
, It would trigger the Stimulus Controller’s #connect method and would open the modal in the page.
Rendering the Button to Trigger the Modal
There are two ways you can trigger the modal. Either with a link_to (GET REQUEST)
or button_to (POST REQUEST)
Triggering from a link_to
Normally you would have a link_to rendered and lets says it would be for rendering a new form to create a Quote
<%= link_to "New", new_quote_path %>
The only thing you would need to do is add this data-turbo-frame -> 'modal'
e.g.
<%= link_to "New", new_quote_path, data: { turbo_frame: 'modal' } >
Now, what this will do basically is instruct Turbo to replace the content of the turbo-frame
with the ID of modal
with the contents from the path of
app/views/quote/new.html.erb
e.g.
<%= render layout: 'layouts/modal',
locals: { title: 'Create a Quote (Inside Modal from GET Request)' } do %>
<%= simple_form_for @quote do |f| %>
<div class="card-body d-flex justify-content-between">
<div>
<%= f.input :name, input_html: { autofocus: true, class: "form-control" } %>
</div>
<div class="d-flex">
<%= f.submit class: "btn btn-success" %>
</div>
</div>
<% end %>
<% end %>
Inside the new.html.erb
, You would then render the modal partial we’ve created and pass in as a block the content you would want to render.
Then that’s it! And you can repeat the 2 steps above whenever you want to render a pop up modal from a link
Triggering from a button_to
Now you can also trigger the pop-up modal after clicking a button. Some use cases would be you would want to render a confirmation pop up upon clicking a button.
You would have a button e.g.
<%= button_to "New", some_post_path %>
Now what you would need to do is create a .turbo_stream
file for the post request path e.g.
# app/views/some_post.turbo_stream.erb
<turbo-stream action="replace" target="modal">
<template>
<%= render layout: 'layouts/modal', locals: { title: 'Modal from a Post Request' } do %>
...
<% end %>
</template>
</turbo-stream>
Why the turbo_stream.erb
file? If you would check the logs, you would see something like this.
Started POST "/some/some_path" for ::1 at 2023-12-18 23:12:51 +0800
Processing by SomeController#some_path as TURBO_STREAM
Note: In Rails 7, Buttons would be considered as a Turbo Stream Request unless data-turbo
is set to false
Now same as the flow from the link_to
, This would replace the turbo-frame with the ID modal with the <template></template>
defined
inside that turbo_stream.erb
file
That’s a wrap! Now you can easily add in any pop up modals within your application.
Thanks for reading, no matter your software problem, we provide a complete solution to scope, design, develop, test, host, maintain and support it 24/7/365. Contact us to find out how can we bring your ideas to reality!