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: