Thursday, December 9, 2010

Create Ajax based CRUD using Rails 3

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.

7 comments:

  1. Hi nikunj its really useful and very good application to know about ajax... keep it up....

    ReplyDelete
  2. thanks for this post, really helpful

    ReplyDelete
  3. Thank you... the only tutorial that I found that actually worked. I wish everyone did tutorials like the way you did. The only part that I wan't 100% sure of was "Create Entry partial(app/views/entries/_entry.html.erb". Just put the address to the file, and the tutorial becomes perfect!

    Thanks again!

    ReplyDelete
  4. Nice Tutorial, only something confuses me which is the statement below...

    $("#new_entry")[0] <--- the function of [0] ?

    ReplyDelete