The three most important factors in determining the value of a home are location, location and location. The three most important factors in choosing a database are performance, performance and performance. While I am open to other arguments, I would say the three most important factors to a web page are performance, performance and performance too. Regardless of how you feel about this last statement, I’m sure we would all agree that improving the performance of our web pages with minimal effort is worth investing in. In this article I will introduce how to use an AJAX modal using Rails. With minimal configuration you can offload all those included modals bloating your DOM while still maintaining the ability to easily leverage them the Rails way.
Note: In this example, I am using Turbolinks v5 , not Turbo v7. Be aware that there are major differences between the two.
Generating a Modal
There are two main ways to generate a modal in Rails. One way is to create it server-side like Bootstrap 4 modal. The second is to render it client-side like SweetAlert. For the purposes of this article, we will focus on generating modal’s server-side in order to take advantage of all the magic Ruby on Rails offers us.
Generating an AJAX Modal
An RoR AJAX Modal is a modal who’s HTML is requested by the client using a XMLHttpRequest, rendered on the server like a normal Rails view, and then returned to the client to display. Using the Bootstrap 4 modal from above, we will fetch our modal’s HTML from the server using a custom Rails action, then insert that HTML into the DOM with and open with some custom.
In other words, we are fetching the HTML for the modal when we need it.
Components of an AJAX Modal using StimulusJS on Rails
For this example we will build an AJAX modal using Bootstrap 4, jQuery, and StimulusJS. All of this code can be found here. https://github.com/davedkg/ajax-modal-using-stimulusjs-on-rails/commit/708250750e07330b571c3fd2da376881116a3b30
Rails Layout
Instead of using views/layouts/application.html.erb, we will use a custom Rails layout for our modals.
<!-- app/views/layouts/modal.html.erb -->
<div class="modal fade">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title pull-left">
Modal Title
</h5>
<a data-dismiss="modal">
X
</a>
</div>
<%= yield %>
</div>
</div>
</div>
Rails Helper
Because its Rails, we want to generate modal links as easily as remote links.
# app/helpers/link_to_helper.rb
# This override adds { modal: true } functionality to our link_to helper.
#
# <%= link_to 'New Post', new_post_path, modal: true %>
#
# turns into
#
# <a
# data-controller="ajax-modal"
# data-action="ajax:success->ajax-modal#success ajax:error->ajax-modal#error"
# data-turbolinks="false"
# data-remote="true"
# href="/posts/new"
# >New Post</a>
module LinkToHelper
# override Rails link_to method
# nothing fancy happening here other than allowing users to pass blocks to link_to
def link_to(name = nil, options = nil, html_options = {}, &block)
if block_given?
html_options = options
options = name
return super(options, merge_modal_html_options(html_options).to_h, &block)
else
super(name, options, merge_modal_html_options(html_options).to_h)
end
end
private
def merge_modal_html_options(html_options)
# check for modal: true
return html_options if nil == html_options || true != html_options[:modal]
# delete modal html option (see line above)
html_options.delete(:modal)
# add remote: true since this is an ajax request
html_options[:remote] = true
# add stimulus data attributes
(html_options[:data] ||= {}).merge!(
# point to stimulus controller
controller: 'ajax-modal',
# wire up stimulus actions
action: [
'ajax:success->ajax-modal#success',
'ajax:error->ajax-modal#error'
].join(" "),
# disable turbolinks (if you are using turbolinks)
turbolinks: false
)
html_options
end
end
StimulusJS Controller
Client-side stimulus controller to respond to success and error events from our AJAX call.
// app/javascript/controllers/ajax_modal_controller.js
import { Controller } from "stimulus"
export default class extends Controller {
success(event) {
const [data, status, xhr] = event.detail
// append modal html to body
const $modal = $(xhr.response).appendTo('body')
// open modal with animation
$modal.modal()
// remove modal html from body after modal closes
$modal.on("hidden.bs.modal", () => $modal.remove())
}
error(event) {
console.log("modal:error", event)
}
}
Using our components to build a simple blog
In this example we will modify a Posts scaffold created by “rails g scaffold Post title body”. A full list of changes can be found here. https://github.com/davedkg/ajax-modal-using-stimulusjs-on-rails/commit/6de8f88e139233fac64b039087e0130bd6d95dc5
PostsController
Since we are using { remote: true } in our form_for helper in _form.html.erb, we will need to respond to create and update using SJRs which means adding support for format.js.
Don’t forget to override the layout with modal for our new and edit actions.
# app/controllers/posts_controller.rb
class PostsController < ApplicationController
layout 'modal', only: [:new, :edit]
# POST /posts or /posts.json
def create
@post = Post.new(post_params)
respond_to do |format|
if @post.save
format.js { redirect_to posts_path, notice: "Post was successfully created." }
else
format.js { render :create }
end
end
end
# PATCH/PUT /posts/1 or /posts/1.json
def update
respond_to do |format|
if @post.update(post_params)
format.js { redirect_to posts_path, notice: "Post was successfully updated." }
else
format.js { render :update }
end
end
end
end
Posts View
All we need to do is add { modal: true} to our link_to methods. Don’t forget to override the layouts for these actions in your controller.
We don’t need to add { remote: true } since our link_to helper override does that when we pass { modal: true }.
<!-- app/views/posts/index.html.erb -->
<table>
<tbody>
<% @posts.each do |post| %>
<tr>
<td><%= post.title %></td>
<td><%= post.body %></td>
<td>
<%= link_to 'Edit', edit_post_path(post), modal: true %>
// <a
// data-controller="ajax-modal"
// data-action="ajax:success->ajax-modal#success ajax:error->ajax-modal#error"
// data-turbolinks="false"
// data-remote="true"
// href="/posts/{:id}/edit"
// >Edit</a>
</td>
</tr>
<% end %>
</tbody>
</table>
<%= link_to 'New Post', new_post_path, modal: true %>
// <a
// data-controller="ajax-modal"
// data-action="ajax:success->ajax-modal#success ajax:error->ajax-modal#error"
// data-turbolinks="false"
// data-remote="true"
// href="/posts/new"
// >New Post</a>
Posts Form
Since our form actions are SJRs, we need to add { remote: true } to our form_for method.
<!-- app/views/posts/_form.html.erb -->
<%= simple_form_for(@post, remote: true) do |f| %>
<div class="modal-body">
<%= f.error_notification %>
<%= f.input :title %>
<%= f.input :body %>
</div>
<div class="modal-footer">
<%= f.button :submit %>
</div>
<% end %>
Posts Action Views
Included for reference.
<!-- app/views/posts/new.html.erb -->
<%= render 'form', post: @post %>
// app/views/posts/create.js.erb
$("#new_post").replaceWith("<%= j render 'form' %>")
<!-- app/views/posts/edit.html.erb -->
<%= render 'form', post: @post %>
// app/views/posts/update.js.erb
$("#edit_post_<%= @post.id %>").replaceWith("<%= j render 'form' %>")
Conclusion
Setting up AJAX modals within a Rails environment is easy. Once you implement it the first time, you can easily use it throughout your entire project. Your users will thank for you those valuable milliseconds they save every time they click a link on your site.