DEV Community

Cover image for Bootstrap Modals Using Hotwire In Rails
Dale Zak
Dale Zak

Posted on

Bootstrap Modals Using Hotwire In Rails

I recently implemented Bootstrap modals on using Hotwire for showing Devise forms. However I ran into a few hurdles so wanted to share the lessons learned and the final implementation. 😎

One of my requirements was to show the modal by default, however when you open the link in a new tab it should still render the regular form inline. I really liked this fallback functionality, so let's see how I got it implemented.

However before diving in, I'd recommend reading How to build modals with Hotwire (Turbo Frames + StimulusJS) and Turbo Frames on Rails, both are excellent articles on using Hotwire. 😍


You'll need to add an empty turbo_frame_tag with id modal to your application.html.erb. This is what we'll use to drop in the modal partial later.

<%= turbo_frame_tag "modal" %>
Enter fullscreen mode Exit fullscreen mode


This partial is the Bootstrap modal, which takes title, body and footer locals, it also attached modal Stimulus controller.

  title ||= "Modal"
  body ||= ""
  footer ||= ""
<div class="modal fade" tabindex="-1" data-controller="modal">
  <div class="modal-dialog">
    <div class="modal-content">
      <div class="modal-header">
        <%= tag.h5 title, class: "modal-title" %>
        <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
      <%= tag.div class: "modal-body" do %>
        <%= render body %>
      <% end if body.present? %>
      <%= tag.div class: "modal-footer" do %>
        <%= render footer %>
      <% end if footer.present? %>
Enter fullscreen mode Exit fullscreen mode

Note, I wanted to pass multiple blocks to the partial, however couldn't find an elegant way to achieve this, so opted to simply pass the path to the partials.


Added a helper so it's convenient to render the partial in our views.

def render_modal(title: "", body: "", footer: "")
  render(partial: '/partials/modal', locals: { title: title, body: body, footer: footer })
Enter fullscreen mode Exit fullscreen mode


This Stimulus controller handles auto showing the modal, removing the .modal-backdrop to avoid layering backdrops if we show another modal, plus removes the contents of the turbo_frame_tag after the modal has been hidden.

import {
} from "bootstrap"
import {
} from "@hotwired/stimulus"
export default class extends Controller {
  connect() {
    let backdrop = document.querySelector(".modal-backdrop");
    if (backdrop) {
    this.modal = new Modal(this.element);;
    this.element.addEventListener('', (event) => {
Enter fullscreen mode Exit fullscreen mode


Big thanks to David Colby for suggesting setting the variant for Turbo Frame requests, it works like magic! 🙌

before_action :turbo_frame_request_variant

def turbo_frame_request_variant
  request.variant = :turbo_frame if turbo_frame_request?
Enter fullscreen mode Exit fullscreen mode


Nothing fancy here, just plain Bootstrap flavored card showing the sessions partial.

<div class="row">
  <div class="col-sm-10 col-md-8 col-lg-6 col-xl-4 offset-sm-1 offset-md-2 offset-lg-3 offset-xl-4">
    <div class="card mb-4">
      <div class="card-header">
        <h2 class="card-title">Sign in</h2>
      <div class="card-body">
        <%= render "devise/sessions/new" %>
      <div class="card-footer text-muted">
        <%= render "devise/shared/links" %>
Enter fullscreen mode Exit fullscreen mode


Because we set request.variant = :turbo_frame in the application_controller, Rails will automatically render this view if it's a Turbo Frame request, amazing! 😊

<%= turbo_frame_tag "modal" do %>
  <%= render_modal title: t('.sign_in'), body: "/devise/sessions/new", footer: "/devise/shared/links" %>
<% end %>
Enter fullscreen mode Exit fullscreen mode


The sessions partial which renders the login form, note I set data: { turbo: false } so it does a full post back when submitting the login form.

<%= form_for(resource, as: resource_name, url: session_path(resource_name), data: { turbo: false }) do |f| %>
  <div class="mb-3">
    <%= f.label :email, class: "form-label" %>
    <%= f.email_field :email, autofocus: true, autocomplete: "email", class: 'form-control' %>
  <div class="mb-3">
    <%= f.label :password, class: "form-label" %>
    <%= f.password_field :password, autocomplete: "current-password", class: 'form-control' %>
  <% if devise_mapping.rememberable? %>
    <div class="mb-3 form-check">
      <%= f.check_box :remember_me, class: 'form-check-input' %>
      <%= f.label :remember_me, class: 'form-check-label' %>
  <% end %>
  <div class="mb-4 mt-4 text-end">
    <%= f.submit t('.sign_in'), class: "btn btn-primary" %>
<% end %>
Enter fullscreen mode Exit fullscreen mode


When linking to new_user_session_path to login, it's important to set data: { turbo: true, turbo_frame: 'modal' }, this will set Hotwire know it's a Turbo Frame request.

<%= link_to "Sign In", new_user_session_path, data: { turbo: true, turbo_frame: 'modal' } %>
Enter fullscreen mode Exit fullscreen mode


In your shared links for Devise, be sure to also set data: { turbo: true, turbo_frame: 'modal' }.

<%= link_to "Sign up", new_registration_path(resource_name), data: { turbo: true, turbo_frame: 'modal' } %>
Enter fullscreen mode Exit fullscreen mode

And that's it! The login form should now be shown as a modal, however when you open the link in a new tab, it renders the regular inline form instead, which is nice fallback logic.

Hotwire Modal

Also clicking the footer links for signup or forgot password, will also show those as modals, and thanks to our Stimulus controller it avoids multiple backdrops getting layer on top each other. 👍

You can checkout the full source code of at Enjoy!

Top comments (0)