Skip to content

Instantly share code, notes, and snippets.

@john-hamnavoe
Last active December 12, 2022 15:51
Show Gist options
  • Save john-hamnavoe/681387b026c92e79bf7de0d7516bb131 to your computer and use it in GitHub Desktop.
Save john-hamnavoe/681387b026c92e79bf7de0d7516bb131 to your computer and use it in GitHub Desktop.
Using Modals in Rails 7 Application using Stimulus and Hotwire

Introduction

Putting modal dialog framework into Rails 7 application. Using this approach by David Colby. But not going as far as it does with dispatch events. As the article notes need to be on Turbo 7.2 at least.

Stimulus Controller

We will create modal_controller.js in app/javascript/controllers. Very similar to excid3 tailwind stimulus components modal component.

// This controller is an edited-to-the-essentials version of the modal component created by @excid3 as part of the essential tailwind-stimulus-components package found here:
// https://github.com/excid3/tailwindcss-stimulus-components

import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
static targets = ['container'];

  connect() {
    this.toggleClass = 'hidden';
    this.backgroundId = 'modal-background';
    this.backgroundHtml = this._backgroundHTML();
  }

  disconnect() {
    this.close();
  }

  open() {
    document.body.classList.add('fixed', 'inset-x-0', 'overflow-hidden');
    this.containerTarget.classList.remove(this.toggleClass);
    document.body.insertAdjacentHTML('beforeend', this.backgroundHtml);
    this.background = document.querySelector(`#${this.backgroundId}`);
  }

  close() {
    if (typeof event !== 'undefined') {
      event.preventDefault()
    }
    this.containerTarget.classList.add(this.toggleClass);
    if (this.background) { this.background.remove() }
  }

  _backgroundHTML() {
    return `<div id="${this.backgroundId}" class="fixed top-0 left-0 w-full h-full" style="background-color: rgba(0, 0, 0, 0.7); z-index: 9998;"></div>`;
  }
}

The register it in app/javascript/controllers\index.js

import ModalController from "./modal_controller"
application.register("modal", ModalController)

Modal Partial

In app/views/shared add _modal.html.erb

<div data-modal-target="container"
     class="hidden fixed inset-0 overflow-y-auto flex items-center  justify-center"
     style="z-index: 9999;">
  <div class="max-w-2xl max-h-screen w-full relative">
    <div class="m-1 bg-white rounded shadow">
      <div class="px-4 py-5 border-b border-gray-200 sm:px-6">
        <h3 id="modal-title" class="text-lg leading-6 font-medium text-gray-900"></h3>
      </div>
      <div id="modal-body"></div>
    </div>
  </div>
</div>

Update application template app/views/layouts/application.html.erb link to the modal data controller and render the modal (initially it will be hidden>

  <body class="h-full" data-controller="modal">
    <!-- REST OF YOUR BODY <%= yeild %> etc. -->
    <%= render "shared/modal" %>
  </body>

Edit Link

Most common scenario is to say edit an item in modal so first change link_to for your edit to this

link_to edit_event_path(event), class:"group flex", data: { action: "click->modal#open", turbo_stream: "" }

Then create turbo_stream file to handle response touch app/views/events/edit.turbo_stream.erb in your views folder

In the turbo_stream.erb add the lines to render the form (pretty much like edit.html) in modal

<%= turbo_stream.update "modal-title", "Edit Event" %>
<%= turbo_stream.update "modal-body", partial: "events/form", locals: { event: @event } %>

Controller Changes

In this scenario the normal controller action on the successful save is fine, it will refresh index. In the event of error need to update the failed path so for example the edit action might look like this with the additional turbo_stream to referesh modal when there is an error

  def update
    respond_to do |format|
      if @event.update(event_params)
        format.html { redirect_to events_path, notice: "Event was successfully updated." }
        format.json { render :show, status: :ok, location: @event }
      else
        format.html { render :edit, status: :unprocessable_entity }
        format.json { render json: @event.errors, status: :unprocessable_entity }
        # add this line...
        format.turbo_stream { render turbo_stream: turbo_stream.replace('event-form', partial: 'form', locals: {event: @event}), status: :unprocessable_entity }
      end
    end
  end

In order for this to work small change needed to the _form.html in the views folder add id e.g. <%= form_with(model: event, id: "event-form") do |form| %> the name corresponding to the replace we had in the controller above.

Form Change

Probably need Cancel/Close button on theform here is need to add data attribute to the link to close model e.g.

link_to "Close", "#", data: {action: "click->modal#close"}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment