DEV Community

Cover image for How to create modals with Rails and Hotwire (and Tailwind CSS)
Rails Designer
Rails Designer

Posted on • Updated on • Originally published at railsdesigner.com

How to create modals with Rails and Hotwire (and Tailwind CSS)

With the introduction of Hotwire in September of 2021, Rails developers could now create more advanced, JavaScript-based UI's without writing hardly any JavaScript.

One of the most common UI elements in modern web-apps is the modal. Creating it with Hotwire is indeed quite straightforward, but there are few gotchas and some things to keep in mind. There have been a few articles and howto's been written about it already, but I'd like to explore a few more (novel) ideas here.

In essence all a modal, sometimes also called a “dialog” is, is a contained component laid on top of the rest of the app (as a side-note: there's a dialog element that can take care of most of the tricky bits too).

So this means with Rails and Hotwire we need two things:

  1. a turbo-frame to load the modals
  2. a wrapper around the actual modal content

The basics

Let's add the turbo-frame to your application's layout.

# app/views/layouts/application.htm.erb
<​%= turbo_frame_tag "modal" %>
Enter fullscreen mode Exit fullscreen mode

Then wrap the view you want as a modal like this:

# app/views/users/new.html.erb

<turbo-frame id="modal">
  # Modal wrapper
  <div role="dialog" tabindex="-1" class="fixed inset-0 z-30 flex items-center justify-center w-full h-screen p-2">

    # Backdrop
    <div class="fixed inset-0 block w-full h-screen cursor-default bg-gray-900/30"></div>

    <div class="relative z-20 w-full max-w-2xl max-h-screen overflow-y-auto bg-white shadow-lg rounded-md">
      # Modal's content, eg. a `form_with` helper
    </div>
  </div>
</turbo-frame>
Enter fullscreen mode Exit fullscreen mode

Then a way to view the modal:

# app/views/users/index.html.erb

<​%= link_to "Create new user", new_user_path, data: {turbo_frame: "modal"} %>
Enter fullscreen mode Exit fullscreen mode

Now when you click the button above, the content from app/views/users.new.html.erb between the turbo-frame tag gets rendered in the turbo_frame_tag "modal" added to the app/views/layouts.html.erb. And because of the Tailwind CSS classes added, the content will “lay on top” of the app.

And that are the basics of getting a modal working in Rails app with Hotwire.

✨ Tip: for a quick way to get more advanced modal's (including Stimulus), check out the modal from Rails Designer, built with ViewComponent, designed with Tailwind CSS and enhanced with Hotwire.

Beyond the basics

Well, those basics were easy enough, but if you opened the modal, you can't close it now… That's bad. We can improve this in few simple ways.

Let's change the backdrop-div to be a button_to, like so:

<​%= button_to nil, nil, type: :button, method: :get, data: {turbo_frame: "modal"}, class: "fixed inset-0 block w-full h-screen cursor-default bg-gray-900/30" %>
Enter fullscreen mode Exit fullscreen mode

While this works, it's not good practice (you can expect the developer console for the reason). Let's up this modal with a simple Stimulus controller.

Enhance the modal with Stimulus

# app/javascript/controllers/modal_controller.js
import { Controller } from "@hotwired/stimulus";

export default class extends Controller {
  connect() {
    this.element.focus();
  }

  hide() {
    this.#modalTurboFrame.src = null;
  }

  hideOnSubmit(event) {
    if (event.detail.success) {
      this.hide();
    }
  }

  disconnect() {
    this.#modalTurboFrame.src = null;
  }

  // private

  get #modalTurboFrame() {
    return document.querySelector("turbo-frame[id='modal']");
  }
}
Enter fullscreen mode Exit fullscreen mode

Then update your app/views/users/new.html.erb like so:

<turbo-frame id="modal">
  < %= tag.button type: :button, data: {action: "modal#hide"}, class: "fixed inset-0 block w-full h-screen cursor-default bg-gray-900/30" %>

  <div data-controller="modal" data-action="turbo:submit-end->modal#hideOnSubmit keydown.esc->modal#hide" role="dialog" tabindex="-1" class="fixed inset-0 z-30 flex items-center justify-center w-full h-screen p-2">
    # …
  </div>
</turbo-frame>
Enter fullscreen mode Exit fullscreen mode

All this Stimulus controller really does is reset the turbo-frame's src attribute, when…

  • …clicking on the backdrop;
  • …a form is successfully submitted;
  • …pressing the Escape key.

And focus the element on connect(); possible because of the tabindex-attribute on the element.

If you want you can create a button inside the modal's content to cancel (something you often see in modals, next to confirmation/save button), like so:

 <​%= button_tag "Cancel", type: :button, method: :get, data: {action: "modal#hide"}, class: "px-3 py-1 text-sm leading-6 font-medium text-gray-700 bg-white border border-gray-200 rounded-md hover:border-gray-300" %>
Enter fullscreen mode Exit fullscreen mode

Notice how it's just a regular Rails button_tag, with a data-action attribute with the value of modal#hide. That's the beauty of Stimulus, small, reusable JavaScript for the HTML you have.

Force a view in a modal

Another thing I'd like to add is force views to be viewed only as a modal (and as a standalone page, like “users/new.html.erb”). This is a tip taken from a previous article about ViewComponents.

It works with a small Rails controller concern:

# app/controllers/concerns/frameable.rb

module Frameable
  extend ActiveSupport::Concern

  private

  def ensure_turbo_frame_response
    redirect_to root_path unless turbo_frame_request?
  end

  def production_environment?
    Rails.env.production?
  end
end
Enter fullscreen mode Exit fullscreen mode

And now over at the UsersController, add this:

# app/controllers/users_controller.rb

class UsersController < ApplicationController
  before_action :ensure_turbo_frame_response, only: %w[new], if: :production_environment?
end
Enter fullscreen mode Exit fullscreen mode

This will ensure users can only view the users/new view through a turbo-frame, ie. as a modal. But only when in production (if: :production_environment?). I like to add this flag because designing the view is quicker standalone than within the modal.

And that's all there's to it to get basic modals working in your Rails app. Feel free to leave your suggestions or ideas below.

Top comments (1)

Collapse
 
sectasy0 profile image
sectasy

Great article! I've crafted a similar piece focusing on dialog boxes—exploring their role in applications, providing insights on proper usage, and detailing implementation through turbo build-in confirm methods.

dev.to/sectasy0/crafting-a-seamles...