Skip to main content

Develop Ajax applications like the pros, Part 2: Using the Prototype JavaScript Framework and script.aculo.us

Michael Galpin (mike.sr@gmail.com), Software architect, eBay
Michael Galpin has been developing Web applications since the late 1990s. He holds a degree in mathematics from the California Institute of Technology and is an architect at eBay in San Jose, CA.

Summary:  Are you building a Web application? Is it supposed to look more like cragislist or flickr? If the answer is the former, then you can probably skip this article. Still reading? Well you are in luck. In this article, Part 2 of a three-part series on JavaScript libraries, you will see how to use the Scriptaculous JavaScript library to enhance your Web applications.

View more content in this series

Date:  24 Jun 2008
Level:  Intermediate PDF:  A4 and Letter (279KB)Get Adobe® Reader®
Activity:  2661 views

This is the second article of a three-part series on popular JavaScript libraries that you can use to create Ajax-powered applications. In Part 1, you learned about the Prototype library to build a Web application for managing songs. In this article, you will use the Scriptaculous library to build a Web application for managing photos.

This article uses the latest version of Scriptaculous, version 1.8.1 (see Resources for a link). Scriptaculous uses the Prototype 1.6 library. Familiarity with JavaScript, HTML, and CSS are a must. This article demonstrates the use of Scriptaculous with Ajax. For the back end, a combination of Ruby on Rails 2.0 and MySQL 5.0.4 were used (see Resources). You can definitely substitute the back-end technologies of your choice with just a few minor adjustments.

Introduction to Scriptaculous

The Scriptaculous JavaScript library is one of the most popular libraries available. It is used to add rich interaction to HTML-based Web sites. It provides numerous visual effects and behaviors that you can pick from to add interactivity to your Web application. Scriptaculous builds on top of the Prototype library.


Figure 1. The Scriptaculous and Prototype stack
The Scriptaculous and Prototype stack

If you read Part 1, then you saw examples of the Ajax abstractions provided by Prototype. Instead of creating its own similar functionality, Scriptaculous simply uses Prototype and adds effects and behaviors on top of it. Scriptaculous provides numerous rich controls such as drag-and-drop elements. It also provides stunning visual effects that you can use in combination with the controls.


Drag-and-drop controls

One of the most useful and visually compelling features you can add to your application is drag-and-drop. The drag-and-drop feature is something we have come to expect out of desktop applications, but it is not seen as often in Web applications. Adding it to your Web application can provide a rich user experience. It may seem like a daunting task, but it is surprisingly easy with Scriptaculous. To demonstrate this, we will take an example application and dissect it to see the benefits of using Scriptaculous.

Example application: The photo organizer

For our example application, we will create an application for managing photos. Our application will let us add photos to a set or delete the photos, all using drag and drop metaphors. To create the back end, we will use Ruby on Rails and MySQL. Ruby on Rails actually includes Scriptaculous by default and provides APIs for using it automatically in your application. We will not use those features from Rails; we will just use it to provide a data service back end.

Photo organizer back end

We will not spend too much time on the back end — just enough to understand it so the operations on the front end will make sense. All of the code is included in the Download section of this article. First, we create the Rails application using rails pix. Next, we need to create a database called pix_development, per the Rails naming convention. You need to modify the config/database.yml file to modify the configuration parameters (host, name, password) to the database.

The most important step is next, which involves creating a scaffolding for the application. Normally, you create scaffolding just to get you started, but in this case we will use it to create a RESTful interface that we can call using Ajax. The command follows: ruby script/generate scaffold photo thumb :string caption :string inSet :boolean. This creates our model with a thumb field (for a thumbnail picture), a caption, and a Boolean indicating if it is in our hypothetical set. Now the command rake db:migrate will create the tables we need in the database. The scaffold generation command also creates a controller for our application. We need to modify this to provide a JSON version of the RESTful service that Rails creates automatically. The modified controller is shown in Listing 1:


Listing 1. The photos Web controller
                
class PhotosController < ApplicationController
  # GET /photos
  # GET /photos.xml
  protect_from_forgery :only => [:create]
  def index
    @photos = Photo.find(:all)

    respond_to do |format|
      format.html # index.html.erb
      format.xml  { render :xml => @photos }
      format.json { render :json => @photos }
    end
  end

  # GET /photos/1
  # GET /photos/1.xml
  def show
    @photo = Photo.find(params[:id])

    respond_to do |format|
      format.html # show.html.erb
      format.xml  { render :xml => @photo }
      format.json { render :json => @photo }
    end
  end

  # GET /photos/new
  # GET /photos/new.xml
  def new
    @photo = Photo.new

    respond_to do |format|
      format.html # new.html.erb
      format.xml  { render :xml => @photo }
      format.json  { render :json => @photo }
    end
  end

  # GET /photos/1/edit
  def edit
    @photo = Photo.find(params[:id])
  end

  # POST /photos
  # POST /photos.xml
  def create
    @photo = Photo.new(params[:photo])

    respond_to do |format|
      if @photo.save
        flash[:notice] = 'Photo was successfully created.'
        format.html { redirect_to(@photo) }
        format.xml  { render :xml => @photo, :status => :created, 
		                                       :location => @photo }
        format.json  { render :json => @photo, :status => :created, 
		                                       :location => @photo }
      else
        format.html { render :action => "new" }
        format.xml  { render :xml => @photo.errors, :status => 
		                                        :unprocessable_entity }
        format.json  { render :json => @photo.errors, :status => 
		                                        :unprocessable_entity }
      end
    end
  end

  # PUT /photos/1
  # PUT /photos/1.xml
  def update
    @photo = Photo.find(params[:id])

    respond_to do |format|
      if @photo.update_attributes(params[:photo])
        flash[:notice] = 'Photo was successfully updated.'
        format.html { redirect_to(@photo) }
        format.xml  { head :ok }
        format.json  { head :ok }
      else
        format.html { render :action => "edit" }
        format.xml  { render :xml => @photo.errors, :status => 
		                                       :unprocessable_entity }
        format.json  { render :json => @photo.errors, :status => 
		                                       :unprocessable_entity }
      end
    end
  end

  # DELETE /photos/1
  # DELETE /photos/1.xml
  def destroy
    @photo = Photo.find(params[:id])
    @photo.destroy

    respond_to do |format|
      format.html { redirect_to(photos_url) }
      format.xml  { head :ok }
      format.json  { head :ok }
    end
  end
end

Notice a few elements here. First, the protect_from_forgery command allows for everything but the create operation to be accessible as a Web service. Next, notice in the respond_to do |format| blocks that we have added format.json { render :json ... }. This uses RoR's built-in support for JSON to respond to a URL that ends with .json. So a /photos.json will call the index method with all the data serialized as JSON. As we saw in the first article in this series, this is a very convenient way to pass data to Prototype. Now we see how the data looks on the back end and how it gets served. Now let's look at the front end and how Scriptaculous is used there.

Photo organizer front end

To understand the front end, let's look at the code and examine how it works. The front end is a simple HTML page, as shown in Listing 2:


Listing 2. The organize.html Web page
                
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN"
   "http://www.w3.org/TR/html4/strict.dtd">

<html lang="en">
<head>
     <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
     <title>Photo Organizer</title>
     <script type="text/javascript" src="javascripts/prototype.js"></script>
     <script type="text/javascript" src="javascripts/effects.js"></script>
     <script type="text/javascript" src="javascripts/dragdrop.js"></script>
     <script type="text/javascript" src="javascripts/builder.js"></script>
     <script type="text/javascript" src="javascripts/organizer.js"></script>
     <script type="text/javascript" src="javascripts/organize_page.js"></script>
     <link rel="stylesheet" href="stylesheets/organizer.css"/>
</head>
<body onLoad="initPage()">
     <div id="pageTitle">Organize Your Photos</div>
     <div id="allPics">
          <span id="setPics"></span>
          <span id="availablePics"></span>
     </div>
          <div id="trash">&nbsp;</div>     
</body>
</html>

This looks fairly simple, right? Note that there are several JavaScript files we loaded. We had to load prototype.js because Scriptaculous uses it (and we will use it as well). Then we used three libraries that are part of Scriptaculous: effects.js, dragdrop.js, and builder.js. Scriptaculous is fairly modular, allowing you to only use what you need. However, the dragdrop.js does have a dependency on effects.js, so make sure you always include them together (if you are using drag-and-drop that is). Of course the most interesting code is in the two JavaScript files that are specific to this application. When the page loads, it calls the initPage function in organize_page.js as shown in Listing 3:


Listing 3. The page initialization JavaScript
                
// the mode for the page
var model = {};

// page initialization
function initPage(){
     var options = {
          method : "get",
          onSuccess : initUi
     }
     new Ajax.Request("photos.json",options);
}
function initUi(xhr){
     model = new Organizer(xhr.responseJSON);
     var list = Builder.node("span", {id: "inList"});
     var list2 = Builder.node("span", {id: "outList"});
     var pix = model.pixList;
     for (var i=0; i < pix.length; i++){
          var pic = buildPicUi(pix[i]);
          if (pix[i].inSet){
               list.appendChild(pic);
          } else {
               list2.appendChild(pic);
          }
     }
     $("setPics").appendChild(list);
     $("availablePics").appendChild(list2);
     // setup drag-and-drop
     makeDraggables();
     initDropZones();
}

The first function, initPage, should look familiar if you have used Prototype before. It simply makes a request back to the server for a list of photos rendered as JSON. It sets the initUi function as the function to call to handle the response from the server. Let's take a look at that function next.

The initUi function takes the data from the server and creates an Organizer object. This object is contained in a separate JavaScript file and acts as a model for our application. You can take a look at that code, which is available in the source code for this article. For now, we will concentrate on the UI components. We dynamically create visual components for two groupings of photos: the ones that are "in" the set and the ones that are not. We use Scriptaculous's Builder library to create the DOM elements for this dynamically. Next we iterate over the photos in our model and create visual representations for them using the buildPicUi function. This function is shown in Listing 4:


Listing 4. The buildPicUi JavaScript function
                
function buildPicUi(pic){
     var picId = "pic" + pic.id;
     var img = Builder.node("img", {src : "images/"+pic.thumb, alt: pic.caption});
     var picNode =  Builder.node("div",{id : picId, class: "pic"},img);
     return picNode;
}

Again, we make use of the Builder library to more easily create our DOM elements. In this case, we create an image (img tag) inside of a div. Now if we go back to the initUi function, we see that we call two other functions, makeDraggables and initDropZones. These functions are shown in Listing 5:


Listing 5. Enabling drag-and-drop
                
function makeDraggables(){
     for (var picId in model.pixMap){
          new Draggable(picId, {revert:true});
     }
}
function initDropZones(){
     Droppables.add("setPics",{onDrop: addToSet, accept: "pic"});
     Droppables.add("availablePics",{onDrop: removeFromSet, accept: "pic"});
     Droppables.add("trash",{onDrop: addToTrash, accept: "pic"});
}

These two functions enable all of the drag-and-drop functionality in the application. The makeDraggables function simply iterates over all of the pictures and creates a Draggable object for each. All that is needed is the DOM ID of the picture (see the buildPicUi function in Listing 4) and then the options. In this case, the only option we have is revert = true. This says that the picture should revert back to its original position unless it lands in a drop zone. The drop zones are created by initDropZones.

The initDropZones takes three areas on the page: the in-list, the out-list, and an area at the bottom designated as trash. For each one, it sets a handler (onDrop) to be called when an object is dropped into the drop zone. It also sets a filter of what kind of objects can be dropped in it. In each case, the drop zone will only accept objects with class "pic." This is the class we set for each picture in the buildPicUi function in Listing 4. So, only pictures can be dropped in our drop zones, and the pictures can only be dragged into a drop zone that will accept them. Each zone has a different handler function. Let's look at the adToTrash handler, shown in Listing 6:


Listing 6. The addToTrash JavaScript function
                
function addToTrash(pic){
     pic.style.left = "";
     pic.style.top = "";
     $("trash").appendChild(pic);          
     Effect.Puff(pic.id, {duration: 0.8});
     model.deletePic(pic.id);
}

This function does several things. First, it cleans up some of the CSS that gets created when you drag an object. Then we add the picture to the "trash" zone. Notice how we used Prototype's convenience notation $("trash"). Next, we use the Scriptaculous effect called Puff. This is a visual effect that shows the picture disappearing to indicate that the picture has been deleted. It is what Scriptaculous calls a combination effect: it uses a combination of the library's core effects. (You could use numerous other effects as well.) Last, we make a call to our model (the Organizer object) to make an Ajax call to delete the object from the database. The Organizer object is shown in Listing 7:


Listing 7. Organizer class
                
var Organizer = Class.create({
     initialize : function(pix){
          this.pixMap = {};
          this.pixList = pix;
          pix.each(function(pic){
               this.pixMap["pic"+pic.id] = pic;
          }.bind(this));
     },
     updatePic : function(picId, data){
          var photo = this.pixMap[picId];
          params = [];
          
          // Rails uses _method since browsers
          // do not properly send HTTP PUT and DELETE
          params["_method"] = "put";
          
          // Rails uses className[propertyName] for
          // request parameters to auto-bind them to
          // object properties
          $H(data).each(function(pair){
               params["photo["+pair.key+"]"] = pair.value;
          });
          var options = {
               method : "post",
               parameters : params
          };
          new Ajax.Request("photos/"+photo.id,options);
     },
     deletePic : function(picId){
          var photo = this.pixMap[picId];
          var params = {};
          params["_method"] = "delete";
          var options = {
               method : "post",
               parameters : params
          };
          new Ajax.Request("photos/"+photo.id,options);                    
     }
});

This class has a lot of interesting things going on. It, like Scriptaculous, makes heavy use of Prototype. It uses Prototype for creating a class (Class.create(...)). Take a look at the initialize function. This is called when you create an Organizer instance. It uses the each function that Prototype adds to JavaScript arrays and objects. This is a familiar construct found in many programming languages but missing in JavaScript. In our example, it is combined with Prototype's bind function. This is a function that Prototype adds to function object in JavaScript. It essentially binds the current context to the function. In this case, we want to be able to alter the Organizer's pixMap field during the execution of the anonymous function being called by the each function. Without the bind, this.pixMap would have no meaning. Also notice that the updatePic function uses another Prototype syntactical shortcut, the $H() notation. This creates a Prototype Hash object that adds things like the eachfunction to our object. Again, this is a familiar construct borrowed from other languages that Prototype brings to JavaScript. Finally, both the updatePic and deletePic once again use Prototype's Ajax.Request function to create a cross-browser-compliant Ajax request to our server.


Running the example

To start the example, you just start up Rails: ruby script/server start for example. Now you can bring it up in your browser as shown in Figure 2:


Figure 2. The photo organizer example
The photo organizer example

When the page loads, you can use a tool like Firebug to watch the asynchronous loading of data, as shown in Figure 3:


Figure 3. Initial data load shown in Firebug
Initial data load shown in Firebug

When you drag pictures from one list to another or to the trash at the bottom of the screen, you should see another Ajax request going out, as shown in Figure 4:


Figure 4. Ajax request triggered by drag-and-drop
Ajax request triggered by drag-and-drop

Now you have a Web application that has a lot of desktop-like functionality. It was built using advanced controls and effects from Scriptaculous, along with simplified Ajax courtesy of Prototype.


Summary

The developerWorks Ajax resource center
Check out the Ajax resource center, your one-stop shop for free tools, code, and information on developing Ajax applications. The active Ajax community forum, hosted by Ajax expert Jack Herrington, will connect you with peers who might just have the answers you're looking for right now.

In this article, Part 2 of this three-part series on JavaScript libraries, we have explored some of the controls and effects provided by the Scriptaculous JavaScript library. We have seen how Scriptaculous builds on top of Prototype to make Ajax development easy. We have also seen how easy it is to combine Scriptaculous controls and effects with Prototype's Ajax to create a rich user experience. We have only scratched the surface, though. Scriptaculous has several other controls and many more effects that you can use in your Web applications, so it's worth it to take the time to explore it further.



Download

DescriptionNameSizeDownload method
Sample codewa-aj-ajaxpro2.zip170KB HTTP

Information about download methods


Resources

Learn

Get products and technologies

Discuss

About the author

Michael Galpin has been developing Web applications since the late 1990s. He holds a degree in mathematics from the California Institute of Technology and is an architect at eBay in San Jose, CA.

Comments (Undergoing maintenance)



Trademarks  |  My developerWorks terms and conditions

Help: Update or add to My dW interests

What's this?

This little timesaver lets you update your My developerWorks profile with just one click! The general subject of this content (AIX and UNIX, Information Management, Lotus, Rational, Tivoli, WebSphere, Java, Linux, Open source, SOA and Web services, Web development, or XML) will be added to the interests section of your profile, if it's not there already. You only need to be logged in to My developerWorks.

And what's the point of adding your interests to your profile? That's how you find other users with the same interests as yours, and see what they're reading and contributing to the community. Your interests also help us recommend relevant developerWorks content to you.

View your My developerWorks profile

Return from help

Help: Remove from My dW interests

What's this?

Removing this interest does not alter your profile, but rather removes this piece of content from a list of all content for which you've indicated interest. In a future enhancement to My developerWorks, you'll be able to see a record of that content.

View your My developerWorks profile

Return from help

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=1
Zone=Web development, Sample IT projects
ArticleID=316036
ArticleTitle=Develop Ajax applications like the pros, Part 2: Using the Prototype JavaScript Framework and script.aculo.us
publish-date=06242008
author1-email=mike.sr@gmail.com
author1-email-cc=ruterbo@us.ibm.com

My developerWorks community

Tags

Help
Use the search field to find all types of content in My developerWorks with that tag.

Use the slider bar to see more or fewer tags.

Popular tags shows the top tags for this particular content zone (for example, Java technology, Linux, WebSphere).

My tags shows your tags for this particular content zone (for example, Java technology, Linux, WebSphere).

Use the search field to find all types of content in My developerWorks with that tag. Popular tags shows the top tags for this particular content zone (for example, Java technology, Linux, WebSphere). My tags shows your tags for this particular content zone (for example, Java technology, Linux, WebSphere).

Rate a product. Write a review.

Special offers