Modals With Hotwire And Stimulus

Charles Martinez December 21, 2023

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.

Modals with 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!