Ajax is well know for handling asynchronous request and Rails is for fast web development. Here I am going to create an application using Rails 3.0.1 which uses Ajax calls for CRUD operations.
1. Create the project
Crate the project address_book. Here I am using sqlite3 to store entries in address book
rails new address_book -d sqlite3
Change directory
cd address_book
Generate scaffold for address book’s Entry.
rails g scaffold Entry name:string address:text phone:string email:string
Now the skeleton for Entry has been created. Use ‘rake’ command to create necessary tables.
rake db:migrate
2. Change entries controller
As we are creating Ajax based CRUD, the application should respond to ‘.js’ format.
respond_to do |format|
format.html
format.js
end
Also we need to modify actions to handle CRUD Ajax requests. Modified entries controller(
app/controllers/entries_controller.rb) looks like:
class EntriesController < ApplicationController
def index
@entry = Entry.new
@entries = Entry.all
respond_to do |format|
format.html
format.js
end
end
def show
@entry = Entry.find(params[:id])
respond_to do |format|
format.html
format.js
end
end
def new
@entry = Entry.new
respond_to do |format|
format.html
format.js
end
end
def edit
@entry = Entry.find(params[:id])
respond_to do |format|
format.html
format.js
end
end
def create
@entry = Entry.new(params[:entry])
respond_to do |format|
if @entry.save
format.html { redirect_to(@entry) }
format.js
else
format.html { render :action => "new" }
end
end
end
def update
@entry = Entry.find(params[:id])
respond_to do |format|
if @entry.update_attributes(params[:entry])
format.html { redirect_to(@entry) }
format.js
else
format.html { render :action => "edit" }
end
end
end
def destroy
@entry = Entry.find(params[:id])
@entry.destroy
respond_to do |format|
format.html { redirect_to(entries_url) }
format.js
end
end
end
3. Change Entry model
Define attributes in Entry model to access fields of entries table. Also put some validations on its fields.
Here is the modified Entry model(
app/models/entry.rb)
class Entry < ActiveRecord::Base
attr_accessible :name, :address, :phone, :email
validates_presence_of :name, :phone, :email
end
4. Change in views
Now we modify index page(
app/views/entries/index.html.erb) to show Entry form.
<h1>Listing entries</h1>
<table id="entries">
<tr>
<th>Name</th>
<th>Address</th>
<th>Phone</th>
<th>Email</th>
<th></th>
<th></th>
<th></th>
</tr>
<% @entries.each do |entry| %>
<%= render entry %>
<% end %>
</table>
<br />
<h2>Entry form</h2>
<div id="form">
<%= render :partial => "form" %>
</div>
In entries's index page you can see that we used id
'entries' with listing table, it is used to manipulate DOM object using jQuery.
Change form partial(
app/views/entries/_form.html.erb) to generate remote POST request.
<%= form_for(@entry, :remote => true) do |f| %>
<div class="field">
<%= f.label :name %><br />
<%= f.text_field :name %>
</div>
<div class="field">
<%= f.label :address %><br />
<%= f.text_area :address, :rows => 3 %>
</div>
<div class="field">
<%= f.label :phone %><br />
<%= f.text_field :phone %>
</div>
<div class="field">
<%= f.label :email %><br />
<%= f.text_field :email %>
</div>
<div class="actions">
<%= f.submit %>
</div>
<% end %>
Create Entry partial(
app/views/entries/_entry.html.erb) to list address book’s Entry.
Before going ahead, we should take care of two things: one is to update index page after receiving response of the Ajax request, and another is to send Ajax request to edit and destroy Entry.
Here we are updating index page by manipulating DOM object using jQuery. But, to manipulate DOM object, each row should be uniquely identified. So, we define unique 'id' attribute with each row.
To send Ajax request for editing and deleting Entry, use ':remote => true' parameter with Edit and Destroy link.
<tr id="<%= dom_id entry %>">
<td><%= entry.name %></td>
<td><%= entry.address %></td>
<td><%= entry.phone %></td>
<td><%= entry.email %></td>
<td><%= link_to 'Show', entry %></td>
<td><%= link_to 'Edit', edit_entry_path(entry), :remote => true %></td>
<td><%= link_to 'Destroy', entry, :confirm => 'Are you sure?', :method => :delete, :remote => true %></td>
</tr>
Now, write javascript templates to update index page.
Create
app/views/entries/create.js.erb template to update the list on adding new Entry and clear the form.
$('<%= escape_javascript(render(:partial => @entry))%>').appendTo('#entries');
$("#new_entry")[0].reset();
Create
app/views/entries/edit.js.erb template to set values into the form.
$("#form > form").replaceWith("<%= escape_javascript(render(:partial => "form"))%>")
Create
app/views/entries/update.js.erb template to update the list with updated Entry, create new Entry and clear the form.
$("#<%= dom_id(@entry) %>").replaceWith("<%= escape_javascript(render(:partial => @entry)) %>");
<% @entry = Entry.new # reset for new form %>
$(".edit_entry").replaceWith("<%= escape_javascript(render(:partial => "form"))%>")
$(".new_entry")[0].reset();
Create
app/views/entries/destroy.js.erb template to delete Entry from the list.
$('#<%= dom_id @entry %>').remove();
Modify application layout (
app/views/layouts/application.html.erb) to include ‘rails’ and ‘application’ javascript.
<!DOCTYPE html>
<html>
<head>
<title>AddressBook</title>
<%= stylesheet_link_tag :all %>
<%= javascript_include_tag :defaults %>
<%= csrf_meta_tag %>
</head>
<body>
<%= yield %>
<%= javascript_include_tag "http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js" %>
<%= javascript_include_tag "rails" %>
<%= javascript_include_tag "application" %>
</body>
</html>
5. Replace ‘rails.js’
Replace rails javascript(
public/javascripts/rails.js) with JQuery.
jQuery(function ($) {
var csrf_token = $('meta[name=csrf-token]').attr('content'),
csrf_param = $('meta[name=csrf-param]').attr('content');
$.fn.extend({
/**
* Triggers a custom event on an element and returns the event result
* this is used to get around not being able to ensure callbacks are placed
* at the end of the chain.
*
* TODO: deprecate with jQuery 1.4.2 release, in favor of subscribing to our
* own events and placing ourselves at the end of the chain.
*/
triggerAndReturn: function (name, data) {
var event = new $.Event(name);
this.trigger(event, data);
return event.result !== false;
},
/**
* Handles execution of remote calls firing overridable events along the way
*/
callRemote: function () {
var el = this,
data = el.is('form') ? el.serializeArray() : [],
method = el.attr('method') || el.attr('data-method') || 'GET',
url = el.attr('action') || el.attr('href');
if (url === undefined) {
throw "No URL specified for remote call (action or href must be present).";
} else {
if (el.triggerAndReturn('ajax:before')) {
$.ajax({
url: url,
data: data,
dataType: 'script',
type: method.toUpperCase(),
beforeSend: function (xhr) {
el.trigger('ajax:loading', xhr);
},
success: function (data, status, xhr) {
el.trigger('ajax:success', [data, status, xhr]);
},
complete: function (xhr) {
el.trigger('ajax:complete', xhr);
},
error: function (xhr, status, error) {
el.trigger('ajax:failure', [xhr, status, error]);
}
});
}
el.trigger('ajax:after');
}
}
});
/**
* confirmation handler
*/
$('a[data-confirm],input[data-confirm]').live('click', function () {
var el = $(this);
if (el.triggerAndReturn('confirm')) {
if (!confirm(el.attr('data-confirm'))) {
return false;
}
}
});
/**
* remote handlers
*/
$('form[data-remote]').live('submit', function (e) {
$(this).callRemote();
e.preventDefault();
});
$('a[data-remote],input[data-remote]').live('click', function (e) {
$(this).callRemote();
e.preventDefault();
});
$('a[data-method]:not([data-remote])').live('click', function (e){
var link = $(this),
href = link.attr('href'),
method = link.attr('data-method'),
form = $('<form method="post" action="'+href+'"></form>'),
metadata_input = '<input name="_method" value="'+method+'" type="hidden" />';
if (csrf_param != null && csrf_token != null) {
metadata_input += '<input name="'+csrf_param+'" value="'+csrf_token+'" type="hidden" />';
}
form.hide()
.append(metadata_input)
.appendTo('body');
e.preventDefault();
form.submit();
});
/**
* disable-with handlers
*/
var disable_with_input_selector = 'input[data-disable-with]';
var disable_with_form_selector = 'form[data-remote]:has(' + disable_with_input_selector + ')';
$(disable_with_form_selector).live('ajax:before', function () {
$(this).find(disable_with_input_selector).each(function () {
var input = $(this);
input.data('enable-with', input.val())
.attr('value', input.attr('data-disable-with'))
.attr('disabled', 'disabled');
});
});
$(disable_with_form_selector).live('ajax:after', function () {
$(this).find(disable_with_input_selector).each(function () {
var input = $(this);
input.removeAttr('disabled')
.val(input.data('enable-with'));
});
});
});
6. Set index page
To set entries index page as the application’s index page, set root entry into
config/routes.rb file.
AddressBook::Application.routes.draw do
resources :entries
root :to => "entries#index"
…
Remove static index page from public folder.
rm public/index.html
Now your application is ready to run.