Hotwire Modals

Episode #335 by Teacher's Avatar David Kimura

Summary

In this episode, we look at creating an unobtrusive and efficient way to launch Bootstrap modals using Turbo and Stimulus.
7.0 rails hotwire turbo stimulusjs 16:13

Resources

Episode Code - https://github.com/driftingruby/335-hotwire-modals

This episode is sponsored by Hook Relay

If you wanted an alternative where you didn't have to support cases with HTML response failovers, this would be even easier with rendering the form_modal within the edit and new html views.

# projects/edit.html.erb & projects/new.html.erb
<%= render partial: "projects/form_modal", locals: { project: @project } %>

With using a turbo_frame data attribute, we can target the remote modal.

# projects/index.html.erb
<%= link_to "Edit", edit_project_path(project), data: { turbo_frame: "remote_modal" } %>
<%= link_to "New project", new_project_path, data: { turbo_frame: "remote_modal" }  %>

This methodology would negate the need for the turbo stimulus controller and the turbo_stream responses.
Download Source Code

Summary

# Terminal
bin/rails g stimulus turbo
bin/rails g stimulus modal

# views/projects/index.html.erb
<%= link_to "Edit", edit_project_path(project), "data-controller": "turbo" %>
<%= link_to "New project", new_project_path, "data-controller": "turbo" %>

# javascript/controllers/turbo_controller.js
import { Controller } from "@hotwired/stimulus"
import { Turbo } from "@hotwired/turbo-rails"

// Connects to data-controller="turbo"
export default class extends Controller {
  initialize() {
    this.element.setAttribute("data-action", "click->turbo#click")
  }

  click(e) {
    e.preventDefault()
    this.url = this.element.getAttribute("href")
    fetch(this.url, {
      headers: {
        Accept: "text/vnd.turbo-stream.html"
      }
    })
    .then(r => r.text())
    .then(html => Turbo.renderStreamMessage(html))
  }
}

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

// Connects to data-controller="modal"
export default class extends Controller {
  connect() {
    this.modal = new bootstrap.Modal(this.element, {
      keyboard: false
    })
    this.modal.show()
  }

  disconnect() {
    this.modal.hide()
  }
}

# app/javascript/application.js
import "@hotwired/turbo-rails"
import "./controllers"
import * as bootstrap from "bootstrap"
window.bootstrap = bootstrap

# views/layouts/application.html.erb
<%= turbo_frame_tag "remote_modal" %>

# views/projects/new.turbo_stream.erb
<%= turbo_stream.replace "remote_modal" do %>
  <%= render partial: "projects/form_modal", locals: { project: @project } %>
<% end %>

# views/projects/edit.turbo_stream.erb
<%= turbo_stream.replace "remote_modal" do %>
  <%= render partial: "projects/form_modal", locals: { project: @project } %>
<% end %>

# view/projects/_form_modal.html.erb
<%= turbo_frame_tag :remote_modal, target: :_top do %>
  <div class="modal fade" data-controller="modal">
    <div class="modal-dialog">
      <div class="modal-content">
        <div class="modal-header">
          <h5 class="modal-title">Modal title</h5>
          <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
        </div>
        <div class="modal-body">
          <%= render "projects/form", project: project %>
        </div>
      </div>
    </div>
  </div>
<% end %>