Rails Ajax

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 myapp -d mysql2

Change directory

cd myapp

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>
    <%= f.label :name %><br />
    <%= f.text_field :name %>
  </div>
  <div>
    <%= f.label :address %><br />
    <%= f.text_area :address, :rows => 3 %>
  </div>
  <div>
    <%= f.label :phone %><br />
    <%= f.text_field :phone %>
  </div>
  <div>
    <%= f.label :email %><br />
    <%= f.text_field :email %>
  </div>
  <div>
    <%= 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 intoconfig/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

Leave a comment

LANGUAGE

Leave a comment