Avoid Inline Javascript with Rails and jQuery

Posted by chris olsen on February 15, 2008

Rails is a great framework and one that allows someone that is new to it get started quickly. I am sure there is many of us that remember the 15 minute blog tutorial, and I must admit, that seemed pretty damn cool the first time I saw it. However after using rails for a while it didn’t take long for some of the automation scripts to lose their appeal. I am now beginning to feel the same in regards to ajax helper methods and .rjs files. They were great at the start because making something cool with rails didn’t require me to learn a javascript library, I was able to just dive right in.

So to get away from all the needless javascript that is auto-created and embedded in the rendered html I decided it was time to read up on one of the many javascript libraries that are available. Even though rails, by default, comes with Prototype and Scriptaculous, I decided to check out jQuery for reasons that are irrelevant to this post.

To quickly test to see how well jQuery would work with Rails I figured that, at the very least, it would have to allow me to do the following:

  1. Provide the same functionality that the link_to_remote helper function does.
  2. Allow for simple form data submissions, ie. a filter from a search box
  3. Enable complex form data submittals

So let’s create a simple project that will just allow us to create a contact list.

rails MyContacts

Now let’s create a table to hold the contact information

script/generate model contact first_name:string last_name:string email:string phone_number:string

And to allow us to get a quick start let’s create some sample data

script/generate migration contact_data
class ContactData < ActiveRecord::Migration
  def self.up
    Contact.create(:first_name => "Joe", :last_name => "Smith", :email => "joe@example.com", :phone_number => "555-3432")
    Contact.create(:first_name => "Sally", :last_name => "White", :email => "sally@example.com", :phone_number => "555-8654")
    Contact.create(:first_name => "Mike", :last_name => "Green", :email => "mike@example.com", :phone_number => "555-6944")
    Contact.create(:first_name => "Mary", :last_name => "Brown", :email => "mary@example.com", :phone_number => "555-2346")
    Contact.create(:first_name => "Alice", :last_name => "Black", :email => "alice@example.com", :phone_number => "555-7866")
    Contact.create(:first_name => "George", :last_name => "Lucas", :email => "george@example.com", :phone_number => "555-1234")
    Contact.create(:first_name => "Jim", :last_name => "Anderson", :email => "jim@example.com", :phone_number => "555-4464")
  end

  def self.down
  end
end

Lastly, let’s create a controller

script/generate controller contacts index show
class ContactsController < ApplicationController
 
  def index
  	@contacts = Contact.find(:all)
  end
 
  def show
    @contact = Contact.find(params[:id])
  end
 
  def search
    filter = params[:filter]
    @contacts = Contact.find(:all, :conditions => ["first_name like ? or last_name like ?", "%#{filter}%", "%#{filter}%"])
    render :action => :index
  end
end

The first item in the required functionality list was to make simple get requests without using the link_to_remote. To do this lets show a list of the users with a link, that when clicked on will show their phone number and email. Below is the code that will allow for the list of contacts to be displayed. I also included the search form that will be used a little bit later.

<!-- contacts/index.html.erb -->
<h1>My Contacts</h1>
<%= link_to "Create Contact", new_contact_url %>
 
<% form_tag search_contacts_url do %>
	Search By Name: <%= text_field_tag :filter %> <%= submit_tag "Search" %>
<% end %>
 
<ul class="contacts">
  <%= render :partial => "contacts/contact", :collection => @contacts %>
</ul>
 
<!-- contacts/_contact.erb -->
<li><%= link_to "#{contact.first_name} #{contact.last_name}", contact_url(contact) %></li>
 
<!-- contacts/show.html.erb -->
<h2><%= "#{@contact.first_name} #{@contact.last_name}" %></h2>
<%= render :partial => "contacts/contact_details", :object => @contact %>
 
<!-- contacts/_contact_details.erb -->
<ul>
	<li>Phone #: <%= contact_details.phone_number %></li>
	<li>Email: <%= contact_details.email %></li>
</ul>
 
<!-- routes.rb -->
map.resources :contacts, :collection => {:search => :post}
 
<!-- web.css -->
body {background-color:#9FDD8F;}
#wrapper {margin:auto; width:760px;}
#centre {float:left; background-color:white; width:760px; padding:15px;}
.contacts {float:left;}
#contact_details {float:left; margin-left:50px;}
  #contact_details dt {float:left; width:60px;}
 
<!-- application.html.erb -->
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
        "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
 
<head>
	<title>My Contacts</title>
	<meta http-equiv="content-type" content="text/html;charset=utf-8" />
	<meta http-equiv="Content-Style-Type" content="text/css" />
	<%= stylesheet_link_tag "web" %>
	<%= javascript_include_tag "jquery-1.2.3.min.js" %>
	<%= yield :javascript %>
</head>
 
<body>
	<div id="wrapper">
		<div id="centre">
			<%= yield :layout %>
		</div>
	</div>
</body>
</html>

If we test what we have out you will see that it works, but having to go to a new page to view the contact’s information, then click the back button to return to the contact list makes me feel rather nauseous. So let’s make the changes to insert the ajax functionality and bring things up to the web 2.0 standards.

Before we get started we will have to download the latest version of jQuery, which can be found on the main page here. Save the file in the public/javascripts folder.

Next we will create a helper method that will allow us to easily insert our custom javascript into the HEAD tag of the page. I have to thank Ryan Bates for letting me know of this method.

# app/helpers/application_helper.rb
def javascript(url)
  content_for :javascript do
    javascript_include_tag url
  end
end

The two blocks of code above allow us to easily fetch the javascript within the js.erb files, since that is where we are going to put it. To insert the javascript file add the following at the top of the index.html.erb file.

<!-- index.html.erb -->
<% javascript formatted_contacts_url("js") %>

This will generate the script tag that will make a request to the controller for the index.js.erb. To allow for this we will also have to update the index method in the Contacts controller. Now our dynamic javascript will be sent back to the client.

def index
 	respond_to do |format|
 	  format.html {@contacts = Contact.find(:all)}
 	  format.js  # returns the index.js.erb   # Add this line of code
 	end
 end

As was mentioned earlier, there were a few things that I wanted to make sure were do-able without too much work. The first one was to make an ajax request much like the link_to_remote helper function. So instead of directing a person to the contact details page, show the details on the contact list page. To do this we will have to make some updates to the controller code to allow it to handle the javascript requests.

def show
  @contact = Contact.find(params[:id])
  respond_to do |format|
    format.html
    format.js {render :partial => "contacts/contact", :object => @contact}
  end
end

As you can see, we are calling on the render method for the partial within the controller code, rather than from the rjs file, to where it normally resides. This will generate the html block of code that we will then insert into the DOM. Before we do that we have to create the javascript that will make the AJAX request, as well as handle the callback. To do this insert the following code in a new index.js.erb file.

// app/views/contacts/index.js.erb
$(document).ready(function(){
	$contact_details = $("<div id='contact_details'></div>").insertAfter($("ul.contacts"))
	$("ul.contacts a").click(function(){
		$.get($(this).attr("href") + ".js", function(data){
			$contact_details.html(data);
		});
		return false;
	});
});

I will am only going to give a brief explanation of the javascript. If you do want to get up to speed with jQuery, this book is very well written and within a few hours you will know your stuff.

On line 2 we bind the DOM loaded event with the inline function ie. the remainder of the code. In line 3 I insert a div tag just after the ul tag to allow the search results to be shown. The reason for this is the div tag that is inserted will only be used to hold the data returned from the ajax request, so it really doesn’t make a lot of sense to hard code it into the html. Line 4 binds click events to all the links within the ul.contacts tag. Line 5 is the function that is bound to the links within the ul tag. This makes a GET request to the same url that is contained within the link to which the event is bound. The second parameter to the $.get() method is the callback function that will insert the returned data into the contact_details div tag.

So far everything is working nicely. We are able to easily view the details for all of our contacts. The next step is to make the updates to allow us to search for contacts via AJAX requests, which can be done with the following code.

First, we will have to update the index.js.html file.

//bind the ajax method to the form
$("form").submit(function(){
	$.post("<%= formatted_search_contacts_url('js') %>", $(this).find("input").serialize(), function(data){
		$(".contacts").html(data);
	});
	return false;
});

Here we bind an inline function to the submit event of the form. The post function takes 3 parameters. The first is the url to post to. Since this code exists in an .erb file and can be dynamically rendered back to the client we are able to use one of the RESTful routes supplied by rails to create the url. Since we are making a javascript request we also have to properly format the url to allow the request to be properly handled by the server. The second parameter consists of the form data. There are a few different ways to pass the data, but the method used here is the easiest. The last parameter is the callback function that will insert the returned data into the page.

Before we go any further I should mention there is another way to do what was done above. There is an additional jQuery plugin that makes form submittal even easier that can be found here. To use this first download the file to the javascript directory, then add the additional javascript file to the head tag.

<%= javascript_include_tag "jquery-1.2.3.min.js", "jquery.form.js" %>

Below is the code that will now allow us to make post requests. The nice thing about this method is that it makes it easier to bind multiple functions to be fired on the beforeSubmit and the success events. I think this code is pretty self-explanatory so I won’t bother going over it. If you want to use the second method mentioned replace the previously mentioned code with the following.

$("form").ajaxForm({
    url: "<%= formatted_search_contacts_url('js') %>", 
		beforeSubmit: function() {alert("This is where we would show our cool little spinner");},
    success: function(data){
         $(".contacts").html(data);
	 return false;
    }
});

With these updates we are now able to search our contact list via AJAX, but there is one catch. If you perform a search to filter the contacts, then click on a contact to view their details you will then be directed to the details page. What is the reason for this you ask? Since our javascript is no longer embedded within a onclick in the link tag that means that responsibility has fallen onto our shoulders, but there are a couple ways to fix this.

The first way is to bind the new items returned in the search results. This method would require a little re-factoring of our current code and isn’t overly hard to do, but there is an easier way. Thanks to event-bubbling all we have to do is bind the event to a parent of the links rather than the links themselves. Update the index.js.erb file to include the following in place of the previous block that bound the click events to the links.

//bind ajax to view the contact details
$("ul").click(function(event){
	$link = $(event.target);
	$.get($link.attr("href") + ".js", function(data){
		$contact_details.html(data);
	});
	return false;
});

Now all the links works as they should after a search is performed.

The last item on my requirement list was to submit a more complex form. As it turns out it can be done using the same method as used in the search example, which is pretty cool. So I am pretty sure that it is safe to avoid repeating myself in another form example.

This wraps up this post regarding avoidance of inline javascript. The html that is returned to the user is clean and it will prevent the user from downloading needless amounts of data in the event that they are accessing the site and have javascript disabled or are unable to use javascript.

Trackbacks

Trackbacks are closed.

Comments

Leave a response

  1. Jerry Jacobsen Sat, 12 Apr 2008 17:36:27 MDT

    I learned a great deal from your post of feb 15. Thank you.

    Could you pleas point me towards some doc regarding the helper method used in

    I can see how it works but can not find doc for it.

    Thanks in advance
    Jerry J

  2. Jerry Jacobsen Sat, 12 Apr 2008 17:46:55 MDT

    ie. in

    index.html.erb

    javascript formatted_contacts_url

  3. Jerry Jacobsen Sun, 13 Apr 2008 06:18:53 MDT

    never mind. found it in

    “http://www.akitaonrails.com/2007/12/12/rolling-with-rails-2-0-the-first-full-tutorial”

Comments