Rails 4 submit modal form via AJAX and render JS response as table row
In this blog post, I’ll demonstrate a common piece of functionality: adding a new model via a modal, submitting via ajax, and rendering a new table row server side.
Initial Rails setup:
mkdir rails4ajaxrender
cd rails4ajaxrender
rails new .
rails g scaffold Person first_name:string last_name:string
rake db:migrate
Added Bootstrap files:
# copied CSS and JS Bootstrap files:
app/assets/javascripts/bootstrap.min.js
app/assets/stylesheets/bootstrap.min.css
Revised app layout to integrate with Bootstrap, file: app/views/layouts/application.html.erb
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Rails4 Ajax Render</title>
<%= stylesheet_link_tag "application", media: "all", "data-turbolinks-track" => true %>
<%= javascript_include_tag "application", "data-turbolinks-track" => true %>
<%= csrf_meta_tags %>
<!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
<!--[if lt IE 9]>
<script src="https://oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script>
<script src="https://oss.maxcdn.com/libs/respond.js/1.4.2/respond.min.js"></script>
<![endif]-->
</head>
<body>
<%= yield %>
</body>
</html>
Added basic model validation, file: app/models/person.rb
class Person < ActiveRecord::Base
validates :first_name, presence: true
validates :last_name, presence: true
end
Revised Person form ERB view, file: app/views/people/_form.html.erb
<%# Conditionally set remote: true. Also, passing model name as data attribute %>
<% modal ||= false %>
<% remote = modal ? true : false %>
<%= form_for(@person, remote: remote, html: {role: :form, 'data-model' => 'person'}) do |f| %>
<% if @person.errors.any? %>
<div id="error_explanation">
<h2><%= pluralize(@person.errors.count, "error") %> prohibited this person from being saved:</h2>
<ul>
<% @person.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
<% end %>
<%# Added Bootstrap classes, and help-block container for error messages %>
<div class="form-group">
<%= f.label :first_name, class: 'control-label' %><br>
<%= f.text_field :first_name, class: 'form-control' %>
<span class="help-block"></span>
</div>
<%# Added Bootstrap classes, and help-block container for error messages %>
<div class="form-group">
<%= f.label :last_name, class: 'control-label' %><br>
<%= f.text_field :last_name, class: 'form-control' %>
<span class="help-block"></span>
</div>
<%# Added Bootstrap classes %>
<div class="actions">
<%= f.submit 'Submit', class: 'btn btn-default' %>
</div>
<% end %>
Added a new partial to render a table row. This will be used in the index ERB partial, and also via AJAX requests. file: app/views/people/_table_row.html.erb
<tr>
<td><%= person.first_name %></td>
<td><%= person.last_name %></td>
<td><%= link_to 'Show', person %></td>
<td><%= link_to 'Edit', edit_person_path(person) %></td>
<td><%= link_to 'Destroy', person, method: :delete, data: { confirm: 'Are you sure?' } %></td>
</tr>
Revised the index ERB view to utilize the new table row partial, and include the hidden Bootstrap modal markup to add a new Person. file: app/views/people/index.html.erb
<h1>Listing people</h1>
<table class='table' id='people_table'>
<thead>
<tr>
<th>First name</th>
<th>Last name</th>
<th></th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
<% @people.each do |person| %>
<%# Render table row partial for each person object %>
<%= render partial: 'table_row', locals: {person: person} %>
<% end %>
</tbody>
</table>
<br>
<%# Added Bootstrap data modal attribute %>
<%= link_to 'New Person', '#new_person_modal', 'data-toggle' => 'modal' %>
<%# Bootstrap modal markup. @see: http://getbootstrap.com/javascript/#modals %>
<div class="modal fade" id="new_person_modal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
<h4 class="modal-title" id="myModalLabel">Create new person</h4>
</div>
<div class="modal-body">
<%# Render the new person form (passing modal => true to enable remote => true) %>
<%= render 'form', modal: true %>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
Edits to People controller: 1. Revised index method to include a new Person object instance variable for the modal form. 2. Added format.js blocks to the create method. Forms that submit via AJAX with “remote: true” default to JS format requests. file: app/controllers/people_controller.rb
class PeopleController < ApplicationController
# ..snip..
def index
@people = Person.all
# added:
@person = Person.new
end
# ..snip..
def create
@person = Person.new(person_params)
respond_to do |format|
if @person.save
format.html { redirect_to @person, notice: 'Person was successfully created.' }
format.json { render action: 'show', status: :created, location: @person }
# added:
format.js { render action: 'show', status: :created, location: @person }
else
format.html { render action: 'new' }
format.json { render json: @person.errors, status: :unprocessable_entity }
# added:
format.js { render json: @person.errors, status: :unprocessable_entity }
end
end
end
# ..snip..
end
Added a new view to render the newly created Person object as a table row via JS. file: app/views/people/show.js.erb
$('#people_table').append("<%= j render partial: 'table_row', locals: {person: @person } %>");
$('#new_person_modal').modal_success();
Added jQuery to bind an event on AJAX error, render the form errors in the modal, and handle the success response to close the modal and reset the form. file: app/assets/javascripts/people.js
$(document).ready(function(){
$(document).bind('ajaxError', 'form#new_person', function(event, jqxhr, settings, exception){
// note: jqxhr.responseJSON undefined, parsing responseText instead
$(event.data).render_form_errors( $.parseJSON(jqxhr.responseText) );
});
});
(function($) {
$.fn.modal_success = function(){
// close modal
this.modal('hide');
// clear form input elements
// todo/note: handle textarea, select, etc
this.find('form input[type="text"]').val('');
// clear error state
this.clear_previous_errors();
};
$.fn.render_form_errors = function(errors){
$form = this;
this.clear_previous_errors();
model = this.data('model');
// show error messages in input form-group help-block
$.each(errors, function(field, messages){
$input = $('input[name="' + model + '[' + field + ']"]');
$input.closest('.form-group').addClass('has-error').find('.help-block').html( messages.join(' & ') );
});
};
$.fn.clear_previous_errors = function(){
$('.form-group.has-error', this).each(function(){
$('.help-block', $(this)).html('');
$(this).removeClass('has-error');
});
}
}(jQuery));
Screenshot in action: