Prototype: Easing AJAX's Pain
April 5, 2006
This article describes Prototype, an open source JavaScript library to create an object for an AJAX application. I explain how to use Prototype by describing an environmentally oriented web application that displays an annual atmospheric carbon dioxide (CO2) level. First, I will discuss Prototype's benefits and describe how to set up Prototype in your application. Second, I will delve into the nitty-gritty of how this application puts the library to good practical use.
Why Prototype?
Why didn't I just create a plain old JavaScript object (POJO) for my application,
instead
of introducing an open source library? For one, Prototype includes a nifty collection
of
JavaScript shortcuts that reduce typing and help avoid the reinvention of the wheel.
The
commonly touted shortcut is $("mydiv")
, which is a Prototype function that
returns a Document Object Model (DOM) Element associated with the HTML tag with
id
"mydiv". That sort of concision alone is probably worth the cost of
setting up Prototype. It's the equivalent of:
document.getElementById("mydiv");
Another useful Prototype shortcut is $F("mySelect")
, for returning the value
of an HTML form element on a web page, such as a selection list. Once you get used
to
Prototype's austere, Perlish syntax, you will use these shortcuts all the time. Prototype
also contains numerous custom objects, methods, and extensions to built-in JavaScript
objects, such as the Enumeration
and Hash
objects (which I discuss
below).
Finally, Prototype also wraps the functionality of XMLHttpRequest
with its own
Ajax.Request
and related objects, so that you don't have to bother with
writing code for instantiating this object for various browsers.
Setting Up
So how do you set up Prototype? First, download it from prototype.conio.net. Prototype is open source and
available under an MIT-style
license. The download includes a file named prototype.js. This is a
JavaScript file that defines the various functions and objects your application will
use.
Add the prototype.js file to your application by using a script
tag in
the HTML code.
<head>... <script src="js/prototype.js" type="text/javascript"> </script> <script src="js/co2.js" type="text/javascript"> </script> <script src="js/eevpapp.js" type="text/javascript"> </script>... </head>
The same directory that holds this application's HTML file contains a directory named js. This js directory contains prototype.js, an object definition in the file co2.js, which this article describes, as well as the rest of the application's client-side code in another file: eevapp.js.
Since the application imports the Prototype file, the JavaScript code in the other
imported
.js files can use Prototype's objects and extensions as if they were declared and
defined locally. JavaScript does not require statements such as import
or
require
to use objects from other JavaScript files that the browser commonly
imports.
Climate Change
How is Prototype going to help with our application's requirements? What are the requirements?
The User Interface is a web browser, such as Safari 1.3, Firefox 1.5, Opera 8.5, or Internet Explorer 6. The application is designed to display a small data set of annual atmospheric carbon dioxide (CO2) levels. This is a number, like 377, that represents the parts-per-million CO2 level in the air for a specific year. I decided to store these years and numbers in a JavaScript object that the browser downloads with the application. It is not necessary to use a database to store this data, which is small in size and needs neither security nor authentication. The data is designed for consumption by the interested public. The application does, however, give this JavaScript object the ability to add new data on the fly when necessary.
CO2 Applet
Figure 1-1 shows the browser screen for the application. In the upper left corner is an applet that displays the CO2 level in the atmosphere when the user chooses a year.
|
This data is derived from the Mauna Loa Observatory, run by the U.S. government in Hawaii. The measurements are taken at high altitude in pristine conditions. Scientists throughout the world use the data in their climate-change research.
There are only 46 different annual levels, associated with the years 1959-2004 (the 2005 level is not available at this time). Therefore, it makes sense to store this information in a JavaScript object. Even if we choose to display the monthly levels associated with those years with this tool (over 500 separate numbers), it still might make sense to store the data on the client. No database or extra server hits are required.
Where Prototype Comes In
The eevapp.js file contains the code that executes when the user selects a year in the selection list widget.
//instantiate an object //defined in the co2.js file var co2lev = new CO2Levels(); //the onload event handler executes //when the browser is finished loading the page. window.onload=function(){ $("co2_select").onchange=function(){ $("co2ppm").innerHTML= co2lev.getYear($F("co2_select")); } };
The value "co2_select"
is the id
value of the selection list that
the user clicks to choose a year.
<select id="co2_select" .../>
The code:
$("co2_select")
returns a DOM Element reference, the equivalent of
document.getElementById("co2_select");
.
The code sets the onchange
event-handler attribute of the selection list
element to a JavaScript function, as in:
$("co2_select").onchange=function(){...}
.
In simpler terms, when the user chooses a year in the select
tag, the browser
executes the defined function. What does this function do? The function gets a reference
to
an HTML div
element with the id
"co2ppm", once again, using
Prototype's shortcut. The innerHTML
property of this div
element,
representing what the user sees in the browser, is set to the return value of a method
call:
co2lev.getYear($F("co2_select"));
The object named co2lev
is instantiated at the top level of the
eevapp.js file.
var co2lev = new CO2Levels();
This object has a method called getYear()
. This method takes a string
representing a year as a parameter, such as "1999"
. The method returns the CO2
level associated with that year.
Pithy Syntax
That dollar sign pops up in the syntax again: $F("co2_select")
. The
$F()
Prototype function returns the value of an HTML form element, in this
case the selection list, when I pass its id
into the method as a parameter. The
application uses this function to change the displayed CO2 level each time a user
chooses a
different year.
One tip to remember with $F()
: it won't return a value from a selection list
unless the list's child option
elements have value
attributes, as
in: <option value="1959">1959</option>
. In other words, at least
the Prototype version I was using (1.4.0) would not return a value with $F()
if
I left out the value="..."
part.
Objective View
The CO2Levels
object definition appears in a different JavaScript file, aptly
named co2.js. Figure 1-2 shows a UML class diagram describing the object.
Figure 1-2. UML class diagram
Here is the entire code for the CO2Levels
object. The first line instantiates
a new object, using syntax derived from Prototype. The levels
local variable is
an object, like an associative array, that links years with their CO2 levels. I have
omitted
most of the years for the sake of readability.
var CO2Levels=Class.create(); CO2Levels.prototype = { /* Source: http://cdiac.esd.ornl.gov/ftp/trends/co2/maunaloa.co2 Mauna Loa Observatory, Hawaii */ initialize: function(){ this.levels={ "1959":315.98,"1960":316.91, "1961":317.65,"1962":318.45, "2003":375.64,"2004":377.38}; this.levelsHash=$H(this.levels); }, getYear: function(year){ if (! isNaN(year)) { return this.levelsHash[year]; } else { return 377; } }, keys: function(){ return this.levelsHash.keys(); }, values: function(){ return this.levelsHash.values(); }, inspect: function(){ alert( this.levelsHash.inspect()); }, add: function(year,level){ var tmp = new Object(); tmp[year] = level; this.levelsHash=this.levelsHash.merge(tmp); } }
Creating Prototype Objects
Using the Class.create()
method in Prototype returns a JavaScript object that
automatically provides new instances of this object with an initialize()
method. This is similar to a constructor method, such as in Java. The
initialize()
method will be called each time the code creates a new
CO2Levels
object. The rest of the code defines the prototype, or blueprint,
for this CO2Levels
object, including the behavior for its
initialize()
method. What does initialize()
do? It creates a
local variable called levels
, which refers to an object that holds all of the
data: the CO2 levels associated with their years. The code then converts this object
to a
Prototype Hash
object, to provide more functionality for the object (such as
the ability to view the object contents and dynamically add new data).
this.levelsHash=$H(this.levels);
Prototype Hash
Object
There's that syntax again: $H()
. This function takes a JavaScript object as
its parameter and returns Prototype's Hash
object. Similar to hash table
structures in other languages, the Hash
has an associative array structure,
along with with several methods that are designed to manipulate its data, as well
as add new
data to the Hash
.
For example, the Hash.keys()
method returns an array of all of the
Hash
's keys (such as all of the years in our data). The values()
method returns an array of values (the CO2 levels). The merge()
method adds new
keys and values to the Hash
.
Delegator
Our own CO2Levels
object uses the concept of delegation, wherein calls to its
own keys()
, values()
, and add()
methods delegate the
real work of these operations to the internal Hash
object. This object is
stored as a local variable: levelsHash.
Let's look at the getYear()
method.
getYear: function(year){ if (! isNaN(year)) { return this.levelsHash[year]; } else { return 377; } }
The built-in JavaScript method isNaN()
returns false if its parameter can be
evaluated as a number (such as "2000"
), and true otherwise (as in
isNaN("hello")
). If the getYear()
parameter passes this test,
then the code uses a common JavaScript expression to return the value of a key or
property:
this.levelsHash[year]
(for example, this.levelsHash["2004"]
evaluates to 377.38). The browser then displays this numerical value inside the HTML
div
element.
Add Stuff to an Existing Hash
The CO2Levels
object has an add()
method, which can add new keys
and values (additional years and CO2 levels) to the existing data.
add: function(year,level){ var tmp = new Object(); tmp[year] = level; this.levelsHash=this.levelsHash.merge(tmp); }
This method creates a new Object
from its two parameters (representing the
year and CO2 level), which might look like: {"2005":381}
The code then passes this object to the Prototype Hash
object's
merge()
method. This method combines the new object with the
Hash
's existing data, essentially merging them into one group of data or
associative array.
The merge()
method returns the existing data with the new property/value
pair(s) appended to the end.
With some refactoring, the code could use XMLHttpRequest
to fetch any new
levels from the Mauna Loa Observatory, then add them to our existing client-side data.
Inspect
Finally, Prototype's Hash
object also has an inspect()
method.
This method creates a readable display of the Hash
's contents, as in Figure
1-3.
Figure 1-3. Looking inside a Hash
The CO2Levels
object delegates the task of its own inspect()
method to its internal Prototype Hash
object.
inspect: function(){ alert( this.levelsHash.inspect()); }
This is a useful debugging tool for viewing the current contents of a Hash
object.
An upcoming article discusses an AJAX caching strategy used with the same application.
It
introduces Prototype's Ajax.Request
object, which reduces the amount of code
that has to be devoted to using the XMLHttpRequest
object. This is the
important top-level JavaScript object used in AJAX applications for making HTTP connections
with a server behind the scenes.