In my journey to build a new Ruby on Rails 7 app, I aim to create something as modern and reactive as a Single Page Application (SPA) written in React. The project I’m currently working on, named AVIMBU, can be explored further here.
A common feature in SPAs is the use of modals. My goal was to find a simple yet powerful and flexible approach to integrate Bootstrap modals without adding them permanently to the DOM, only revealing them upon a button click. Fortunately, Turbo Frames offer a solution to transfer HTML over the wire, allowing us to display modals without rerendering the entire page.
This blog post provides a step-by-step guide on integrating a Bootstrap 5 modal in a Ruby on Rails 7 application using Turbo Frames, thus enhancing your application’s interactivity without full page reloads.
In order to create a barebones Rails 7 project with Bootstrap preconfigured, run the following command:
rails new bootstrap_modal --css=bootstrap -j=esbuild
This command creates a Rails project with the latest version of Bootstrap preconfigured. For JavaScript packaging, we will use esbuild
, which is straightforward and simple to use. You could also opt for using webpacker
or the more modern import_maps
approach, however, for now it’s easiest to stick with esbuild
due to its straightforward integration of Bootstrap.
After project creation, navigate to the project directory and start it with:
bin/dev
Your application should now be available at localhost:3000
.
This initial setup comes with Turbo enabled. Verify this by checking for the following line in app/javascript/application.js
:
import "@hotwired/turbo-rails"
In order to achieve a reactive and modern-feeling web application, we’ll utilize Turbo Frames. Turbo Frames, a key component of Hotwire, allow us to define which parts of an application’s HTML should be replaced, rather than doing a full page reload.
To use them, add the following line to a view file:
<%= turbo_frame_tag "modal", target: "_top" %>
I added this line to my application.html.erb
file, right above the <%= yield %>
part. This allows for adding my modal HTML element to this frame. The turbo_frame_tag
creates a div
element with the class set to modal
, which Turbo uses to replace the content of this div with the new Turbo Frame content. The target: "_top"
attribute allows for conventional redirects (e.g., on model creation).
From the Bootstrap documentation, you can find some skeleton Bootstrap 5 modal examples here. I used one of them and created a _modal.html.erb
partial for later reuse. Ensure to copy an "active" modal instead of a hidden one. The partial looks like this:
<%= turbo_frame_tag "modal" do %> <div class="modal fade" tabindex="-1" data-controller="modal"> <div class="modal-dialog modal-dialog-centered modal-lg"> <div class="modal-content"> <div class="modal-header"> <div class= "modal-title fs-3"> <%= title %> </div> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> </div> <div class="modal-body" id="remote_modal_body" > <%= yield %> </div> </div> </div> </div> <% end %>
Since the Bootstrap modal relies on some JavaScript logic, I added a few lines of JavaScript to ensure proper background handling. For this, I chose to use a Stimulus controller as it’s the easiest solution for handling JavaScript within a modern Rails 7 application with Turbo. The controller looks like this:
import { Controller } from "@hotwired/stimulus" import { Modal } from "bootstrap"; // Connects to data-controller="modal" export default class extends Controller { connect() { let backdrop = document.querySelector(".modal-backdrop"); if (backdrop) { backdrop.remove(); } this.modal = new Modal(this.element); this.modal.show(); this.element.addEventListener("hidden.bs.modal", (event) => { this.element.remove(); }); } }
As seen in my _modal.html.erb
partial, I've added the data-controller="modal"
parameter to link this Stimulus controller with the modal HTML. This controller ensures that the modal backdrop is handled correctly by properly removing and showing the Modal element.
Here’s the crucial part of integrating Turbo Frames into our Controller actions. I created a simple example scaffold for some model logic:
rails g scaffold project name description
This command creates all necessary controllers, models, and views to create, update, or delete Project models. Change your root route to the index action of the Project controller by adding this to routes.rb
:
root "projects#index"
Now, when you open your application, you’ll see the index page of the Project model, allowing you to create some Project models. However, the new and edit actions redirect to their respective .html.erb
pages. We want them to open within a modal. This can be done by adapting the view files as follows:
<%= render 'partials/modal', title: "New Project", fade_in: true do %> <%= render "form", project: @project %> <% end %>
Do the same for the edit.html.erb
file. Now, when you try to create or edit a model, the view renders within a Bootstrap modal 🥳
If you add model validations, such as requiring a name, you might notice that errors are not properly handled within the modal.
class Project < ApplicationRecord validates :name, presence: true end
In order to improve this, we need to change how the controller responds in case of an error for both the create and update actions:
# POST /projects or /projects.json def create @project = Project.new(project_params) if @project.save redirect_to project_url(@project), notice: "Project was successfully created." else render :form_update, status: :unprocessable_entity end end # PATCH/PUT /projects/1 or /projects/1.json def update if @project.update(project_params) redirect_to project_url(@project), notice: "Project was successfully updated." else render :form_update, status: :unprocessable_entity end end
This adds a custom render
response for error cases, instructing the controller to render the form_update
view. This view will update the modal's body via a Turbo Stream (see Turbo Streams documentation) to show the correct error and avoid a full page reload. The form_update.turbo_stream.erb
file looks like this:
<%= turbo_stream.update "remote_modal_body" do %> <%= render "form", project: @project %> <% end %>
Now, if you try to save a Project model without a name, the error is shown within the modal:
Modal with Error
That’s it! We’ve successfully implemented a Bootstrap modal using Turbo Frames, making our Rails 7 application feel more responsive and modern.
The final product should look like this:
Final Product
We’ve covered how to integrate a Bootstrap 5 modal in a Ruby on Rails 7 application using Turbo Frames. This integration allows for a more dynamic and responsive user interface without the need for full page reloads. Experiment with Turbo Frames and Bootstrap modals, and feel free to share your experiences or ask questions in the comments below.
If you want to know more about me, feel free to check out my homepage or follow me on Twitter here.
Bootstrap 5 — a developer friendly CSS component library
The code used in this tutorial can be found here.
Alternative resources: