Modal Forms
Steps
- Prepare Your Bundle
- Modifying your layout files
- Creating modal.js
- Creating the Modal Responder
- Modifying the Application Controller
- Time to Use It!
Step 0 - Prepare Your Bundle
Make sure that your project is using at least rails 4.2
and at least responders 2.0
.
The responders
gem adds the methods respond_with
and respond_to
.
gem 'rails', '~> 4.2'
gem 'responders', '~> 2.0'
Other Dependencies
bootstrap
and jquery
are also required for this, so it is necessary to add them to your asset pipeline.
gem 'bootstrap'
gem 'jquery-rails'
Step 1 - Modifying your layout files
First we need to define the layout for our modal.
<%# app/views/layouts/modal.html.erb %>
<div class="modal" id="mainModal" tabindex="-1" role="dialog" aria-labelledby="mainModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="mainModalLabel">
<%= yield :title if content_for? :title %>
</h5>
<button type="button" class="close" data-dismiss="modal"><span aria-hidden="true">×</span><span class="sr-only">Close</span></button>
</div>
<%= yield %>
</div>
</div>
</div>
We also need to define where our modal will be rendered. For that we will place this in our application
layout.
<%# app/views/layouts/application.html.erb %>
<div id="modal-holder"></div>
Step 2 - Creating modal.js
Now we want to create the javascript that will render our links that have the data-modal
attribute in our modal layout.
As well we will need to create the functionality that will allow our remote forms to submit data back to the appropriate controller. Our application should be able to properly handle redirects to the given page and form re-displays with errors just as our normal forms would. We will assume that if the response has the location
header set, that we will redirect the user to the specified location. Otherwise we will redisplay the form.
// app/assets/javascripts/modals.js
$(function () {
const modal_holder_selector = "#modal-holder";
const modal_selector = ".modal";
$(document).on("click", "a[data-modal]", function () {
const location = $(this).attr("href");
// Load modal dialog from server
$.get(location, (data) => {
$(modal_holder_selector).html(data).find(modal_selector).modal();
});
return false;
});
$(document).on("ajax:success", "form[data-modal]", function (event) {
const [data, _status, xhr] = event.detail;
const url = xhr.getResponseHeader("Location");
if (url) {
// Redirect to url
window.location = url;
} else {
// Remove old modal backdrop
$(".modal-backdrop").remove();
// Update modal content
const modal = $(data).find("body").html();
$(modal_holder_selector).html(modal).find(modal_selector).modal();
}
return false;
});
});
Step 3 - Creating the Modal Responder
In this class we will inherit from ActionController::Responder
since that is what respond_with
uses for its rendering. We will make our own custom class to handle rendering for our modal, which we will call ModalResponder
.
We will override the render
and redirect_to
methods in order to modify the behavior when a request is made using XHR.
Now if a request is made via AJAX we will render
using out custom modal
layout and instead of redirecting we want redirect_to
to return only headers with the location
header set which will handle our javascript logic.
# app/services/modal_responder.rb
class ModalResponder < ActionController::Responder
cattr_accessor :modal_layout
self.modal_layout = 'modal'
def render(*args)
options = args.extract_options!
if request.xhr?
options.merge! layout: modal_layout
end
controller.render *args, options
end
def default_render(*args)
render(*args)
end
def redirect_to(options)
if request.xhr?
head :ok, location: controller.url_for(options)
else
controller.redirect_to(options)
end
end
end
Step 4 - Modifying the Application Controller
Now we just need to add a helper method, respond_modal_with
, that will use our ModalResponder
.
class ApplicationController < ActionController::Base
respond_to :html
# Prevent CSRF attacks by raising an exception.
# For APIs, you may want to use :null_session instead.
protect_from_forgery with: :exception
def respond_modal_with(*args, &blk)
options = args.extract_options!
options[:responder] = ModalResponder
respond_with *args, options, &blk
end
end
Step 5 - Time to Use It!
First we need to add a link to the modal:
<%# app/views/layouts/_header.html.erb %>
<%= link_to 'Add Message', new_message_path, class: 'btn btn-outline-success my-2 my-sm-0', data: { modal: true } %>
Now, we need to modify our controller using our new respond_modal_with
method instead of respond_with
:
# app/controllers/messages_controller.rb
class MessagesController < ApplicationController
respond_to :html, :json
def new
@message = Message.new
respond_modal_with @message
end
def create
@message = Message.create(message_params)
respond_modal_with @message, location: messages_path
end
private
def set_message
@message = Message.find(params[:id])
end
def message_params
params.require(:message).permit(:name, :body)
end
end
And, finally, you should add two attributes to your form:
<%# app/views/messages/_form.html.erb %>
<%= simple_form_for(@message, remote: request.xhr?, html: { data: { modal: true } }) %>
remote
tells jquery_ujs
to submit this form with AJAX. We use request.xhr?
because we want this form to be functional when we display it in a modal and separately.
data-modal
is used to tell our script to handle this form as the modal form.