Super easy modals with Rails and Stimulus (and a bit of Tailwind)

There’s no doubt that Rails makes it fun to build web apps. Together with Turbolinks and StimulusJS these apps can enjoy some snappiness and pleasant interactivity without the bulkiness JS frameworks like React or Vue add.

One of those nice interactions is the modal. This can be one in the literal sense you might know it, eg. “Subscribe to our newsletter” or the layer-kind that slides in from the right and are often seen in modern SaaS apps UI. The latter example provides a modern and clean way for your users to interact with your content. And by combining the tools Rails gives us, this is quick and easy to set up.

Let’s go over the different pieces of code needed to add this to your app.

The Stimulus controller:

import { Controller } from 'stimulus'

export default class extends Controller {
  static targets = [ 'wrapper', 'container', 'content', 'background']

  initialize() {
    this.url = this.data.get('url')
  }

  view(e) {
    if (e.target !== this.wrapperTarget &&
      !this.wrapperTarget.contains(e.target)) return

    if (this.open) {
      this.getContent(this.url)
      this.wrapperTarget.insertAdjacentHTML('afterbegin', this.template())
    }
  }

  close(e) {
    e.preventDefault()

    if (this.open) {
      if (this.hasContainerTarget) { this.containerTarget.remove() }
    }
  }

  closeBackground(e) {
    if (e.target === this.backgroundTarget) { this.close(e) }
  }

  closeWithKeyboard(e) {
    if (e.keyCode === 27) {
      this.close(e)
    }
  }

  getContent(url) {
    fetch(url).
      then(response => {
        if (response.ok) {
          return response.text()
        }
      })
      .then(html => {
        this.contentTarget.innerHTML = html
      })
  }

  template() {
    return `
      <div data-target='remote.container'>
        <div class='modal-wrapper' data-target='remote.background' data-action='click->remote#closeBackground'>
          <div class='fixed top-0 w-full bg-white rounded z-20 overflow-auto shadow-xl' data-target='remote.content'>
          <span class='loader-spinner m-2'></span>
        </div>

        <button data-action='click->remote#close' class='absolute top-0 right-0 w-6 h-6 m-1 text-white z-20 m-2'>
          <svg width='24' height='24' viewBox='0 0 24 24' fill='none'>
            <path d='M6 18L18 6M6 6L18 18' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' />
          </svg>
        </button>
        </div>
      </div>
    `
  }

We first initialise a url to load which is set in the HTML (data-remote-url). Then we add two main functions “view” and “close”. “View” loads the content and inserts the HTML into the “wrapperTarget”. The ”close” function will simply undo all this. The template() function renders the actual modal UI, it uses TailwindCss classes to be near “copy-pasteable™”.

You are free to add extra code, like adding overflow: hidden to the body when the modal is open, changing the URL to match the opened state modal, show an error when something went wrong, etc. But this given code is enough to get you started.

The line to add to your controller action:

def show
  …
  render layout: false if params[:inline] == 'true'
  …
end

For every action you load in your modal, you should not render the layout of that page, otherwise it would show elements like navigation, footer and whatever else you have in your layout (eg. app/views/layouts/application.html.erb). By making it conditional (if params[:inline] == 'true' you can still render the page standalone outside of this Javascript modal). You can also choose to opt-in to load a separate page which does not render the layout by default, if you don’t care to load the page without javascript.

The HTML needed to view and close your modal:

<div
  data-controller='remote'
  data-target='remote.wrapper'
  data-action='keydown@window->remote#closeWithKeyboard'
  data-remote-url='<%= root_path(inline: true) %>'>
    <button data-action='remote#view'>Show remote content</button>
</div>

All basic Stimulus setup here. The most interesting part is the extra params (inline: true) added to the data-remote-url attribute (which we check for in the controller action).

And that’s it! These pieces of code are ready to be copy-pasted into your codebase. And within minutes you have a solid new UI component that looks and functions like many of the more bloated JS frameworks provide (but without the headache and bloat!).

A Rails SaaS starter kit to build successful products

Get started

25+ developers, companies and freelancers bought Sjabloon in the last 4 weeks