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">&times;</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:

Rails AJAX modal