At the Forge - Prototype
During the last few months, we have looked at ways to use JavaScript, a version of which is included in nearly every modern Web browser. For most of its life, JavaScript has been used to create simple client-side effects and actions on Web pages. But during the past year or two, JavaScript has taken center stage as part of the Ajax (Asynchronous JavaScript and XML) paradigm. It is no longer enough to create Web applications that reside on the server. Modern Web applications must include Ajax-style behavior, which probably means integrating JavaScript into the mix of server-side programs, HTML and relational databases.
As we have seen in the last few installments of this column, however, using JavaScript requires a fair amount of repeated code. How many times must I invoke document.getElementById(), just to grab nodes that I want to modify? Why must I create a library that handles the basic Ajax calls that I will be making on a regular basis? Must I create all of my own widgets and graphic effects?
Fortunately for Web developers everywhere, the explosive interest in Ajax has led to equally productive work on libraries to answer these questions and needs. Many of these libraries have been released under open-source licenses and are thus available for Web developers to include in a variety of different types of sites.
This month, we look at one of the best-known JavaScript libraries, known as Prototype. Prototype, developed by Sam Stephenson (a member of the Ruby on Rails core team), has been included in all copies of Ruby on Rails for some time. Prototype aims to make it easier to work with JavaScript, offering a number of shortcuts for some of the most common uses.
If you are using Ruby on Rails for your Web applications, Prototype is already included. You can begin to use it in your applications by adding the following inside a Rails view template:
<%= javascript_include_tag 'prototype' %>
If you are not using Rails, you still can use Prototype. Simply download it from its site (see the on-line Resources). Then use:
<script type="text/javascript" src="/javascript/prototype.js"></script>
The above assumes, of course, that you have put prototype.js in the /javascript URL on your Web server. You might have to adjust that URL to reflect the configuration of your system.
Once you have included Prototype, you can start to take advantage of its functionality right away. For example, Listing 1 shows simpletext.html. This file contains some simple JavaScript that changes the headline to the contents of the text field when you click on the submit button.
Listing 1. simpletext.html
<!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"> <head><title>Title</title> <script type="text/javascript"> function removeText(node) { if (node != null) { if (node.childNodes) { for (var i=0 ; i < node.childNodes.length ; i++) { var oldTextNode = node.childNodes[i]; if (oldTextNode.nodeValue != null) { node.removeChild(oldTextNode); } } } } } function appendText(node, text) { var newTextNode = document.createTextNode(text); node.appendChild(newTextNode); } function setText(node, text) { removeText(node); appendText(node, text); } function setHeadline () { var headline = document.getElementById("headline"); var fieldContents = document.forms[0].field1.value; setText(headline, fieldContents); } </script> </head> <body> <h2 id="headline">Simple form</h2> <form id="the-form" action="/cgi-bin/foo.pl" method="post"> <p>Field1: <input type="text" id="field1" name="field1" /></p> <p><input type="button" value="Change headline" onclick="setHeadline()"/></p> </form> </body> </html>
We do this by defining a function (setHeadline) and then by setting that function to be invoked when we click on the button:
<p><input type="button" value="Change headline" onclick="setHeadline()"/></p>
Now, what happens inside setHeadline? First, we grab the node containing the headline:
var headline = document.getElementById("headline");
Then, we get the contents of the text field, which we have called field1:
var fieldContents = document.forms[0].field1.value;
Notice how we must grab the value by going through the document hierarchy. First, we get the array of forms from the document (document.forms), then we grab the first form (forms[0]), then we grab the text field (field1), and then we finally get the value.
Now we can set the value of the headline by attaching a text node to the h2 node. We do this with a function called setText, which I have included in simpletext.html; setText depends in turn on removeText and appendText, two other helper functions that make it easy to work with text nodes in JavaScript.
All of this is very nice and is typical of the type of JavaScript coding I often do. How can Prototype help us? By simplifying our code using two built-in functions. The first, $(), looks a bit strange but is legitimate—its full name is $ (dollar sign), and it performs much the same task as document.getElementById, returning the node whose ID matches its parameter. The second, $F, returns the value from the form element whose ID matches the parameter.
In other words, we can rewrite our function as:
function setHeadline() { var headline = $("headline"); var fieldContents = $F("field1"); setText(headline, fieldContents); }
Sure enough, this works just as well as the previous version. However, it's a bit easier to read (in my opinion), and it allows us to avoid traversing the document hierarchy until we reach the form element.
We can improve our code even further by removing our setText, updateText and removeText functions, all of which were included simply because JavaScript doesn't provide any easy way to manipulate the text of a node. But Prototype does through its Element class, allowing us to rewrite setHeadline as:
function setHeadline() { Element.update($("headline"), $F("field1")); }
The code invokes Element.update, handing it two parameters: the node whose text we want to modify and the text we want to insert in place of the current text. We have just replaced 30 lines of our code with one line, thanks to Prototype. You can see the result in Listing 2.
Listing 2. simpletext-prototype.html
<!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"> <head><title>Title</title> <script type="text/javascript" src="prototype.js"></script> <script type="text/javascript"> function setHeadline() { Element.update($("headline"), $F("field1")); } </script> </head> <body> <h2 id="headline">Simple form</h2> <form id="the-form" action="/cgi-bin/foo.pl" method="post"> <p>Field1: <input type="text" id="field1" name="field1" /></p> <p><input type="button" value="Change headline" onclick="setHeadline()"/></p> </form> </body> </html>
The $() function is more than merely a terse replacement for document.getElementById(). If we hand it multiple IDs, it returns an array of nodes with those IDs. For example, we can add a second headline and then set them both with the following code:
function setHeadline() { var headlines = $("headline", "empty-headline"); for (i=0; i<headlines.length; i++) { Element.update(headlines[i], $F("field1")); } }
Whereas there is only text in the headline node when the page is loaded, pressing the button results in setting both headline and empty-headline to the contents of the field1 field.
Prototype brings much more to the table than $(), $F() and a few convenience classes. You can think of it as a grab-bag of different utility functions and objects that make JavaScript coding easier.
For example, in our above definition of setHeadline, we had the following loop:
for (i=0; i<headlines.length; i++) { Element.update(headlines[i], $F("field1")); }
This should look familiar to anyone who has programmed in C, Java or Perl. However, modern programming languages (including Java) often support enumerators or iterators, for more expressive and compact loops without an index variable (i, in the above loop). For example, this is how we can loop over an array in Ruby:
array_of_names = ['Atara', 'Shikma', 'Amotz'] array_of_names.each do |name| print name, "\n" end
Prototype brings Ruby-style loops to JavaScript, by defining the Enumerator class and then providing its functionality to the built-in Array object. We thus could rewrite our setHeadline function as:
function setHeadline() { var headlines = $("headline", "empty-headline"); headlines.each( function(headline) { Element.update(headline, $F("field1")); } ); }
This code might look a bit odd, half like Ruby and half like JavaScript. In addition, it might seem strange for us to be defining a function inside of a loop, which is itself executing inside of a function. However, one of the nice features of JavaScript, like many other modern high-level languages, is that functions are first-class objects, which we can create and pass around exactly like any other type of object. Just as you wouldn't be nervous about creating an array inside of a loop, you shouldn't be nervous about defining a function inside of a loop.
I should also note that the each method provided by Prototype's Enumerated object takes an optional index argument, which counts the iterations. So, we can say:
function setHeadline() { var headlines = $("headline", "empty-headline"); headlines.each( function(headline, index) { Element.update(headline, index + " " + $F("field1")); } ); }
Now, each headline will appear as before, but with a number prepended to the text. Listing 3 shows the resulting page.
Listing 3. simpletext-each.html
<!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"> <head><title>Title</title> <script type="text/javascript" src="prototype.js"></script> <script type="text/javascript"> function setHeadline() { var headlines = $("headline", "empty-headline"); headlines.each( function(headline, index) { Element.update(headline, index + " " + $F("field1")); } ); } </script> </head> <body> <h2 id="headline">Simple form</h2> <h2 id="empty-headline"></h2> <form id="the-form" action="/cgi-bin/foo.pl" method="post"> <p>Field1: <input type="text" id="field1" name="field1" /></p> <p><input type="button" value="Change headline" onclick="setHeadline()"/></p> </form> </body> </html>
Prototype provides additional methods for Enumerable objects, such as all find (to locate an object for which a function returns true); inject (to combine the items using a function, useful for summing numbers); min/max (to find the minimum or maximum value in a collection); and map (to apply a function to each member of a collection). These methods are available not only for arrays, but also for Hash and ObjectRangle, two classes that come with Prototype.
One of the most common reasons for the recent interest in JavaScript is the growing interest in Web applications that incorporate Ajax techniques. As we have seen in the last few installments of this column, Ajax is nothing more than 1) creating an XmlHttpRequest object, 2) writing a function that sends the HTTP request with that object, 3) setting the event handler to invoke that function, and 4) writing a function that is invoked when the HTTP response returns. It isn't particularly difficult to deal with all of these things in code, but why should you be creating XmlHttpRequest objects at all, when you could be concentrating on higher-level concerns?
Listing 4. post-ajax-register.html
<!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"> <head><title>Register</title> <script type="text/javascript"> function getXMLHttpRequest () { try { return new ActiveXObject("Msxml2.XMLHTTP"); } catch(e) {}; try { return new ActiveXObject("Microsoft.XMLHTTP"); } catch(e) {} try { return new XMLHttpRequest(); } catch(e) {}; return null; } function removeText(node) { if (node != null) { if (node.childNodes) { for (var i=0 ; i < node.childNodes.length ; i++) { var oldTextNode = node.childNodes[i]; if (oldTextNode.nodeValue != null) { node.removeChild(oldTextNode); } } } } } function appendText(node, text) { var newTextNode = document.createTextNode(text); node.appendChild(newTextNode); } function setText(node, text) { removeText(node); appendText(node, text); } var xhr = getXMLHttpRequest(); function parseResponse() { // Get variables ready var response = ""; var new_username = document.forms[0].username.value; var warning = document.getElementById("warning"); var submit_button = document.getElementById("submit-button"); // Wait for the HTTP response if (xhr.readyState == 4) { if (xhr.status == 200) { response = xhr.responseText; switch (response) { case "yes": setText(warning, "Warning: username '" + new_username +"' was taken!"); submit_button.disabled = true; break; case "no": removeText(warning); submit_button.disabled = false; break; case "": break; default: alert("Unexpected response '" + response + "'"); } } else { alert("problem: xhr.status = " + xhr.status); } } } function checkUsername() { // Send the HTTP request xhr.open("POST", "/cgi-bin/check-name-exists.pl", true); xhr.onreadystatechange = parseResponse; var username = document.forms[0].username.value; xhr.send("username=" + escape(username)); } </script> </head> <body> <h2>Register</h2> <p id="warning"></p> <form action="/cgi-bin/register.pl" method="post" enctype="application/x-www-form-urlencoded"> <p>Username: <input type="text" name="username" onchange="checkUsername()" /></p> <p>Password: <input type="password" name="password" /></p> <p>E-mail address: <input type="text" name="email_address" /></p> <p><input type="submit" value="Register" id="submit-button" /></p> </form> </body> </html>
Fortunately, Prototype includes objects and functionality that make Ajax programming quite easy. For example, last month's column showed how we could use Ajax to check whether a user name was already taken when an individual registers for a Web site, which I show in Listing 4. The idea is that when someone enters a user name, we immediately fire off a request to the server. The server's response will tell us whether the user name has been taken. We invoke our Ajax request by setting the username field's onchange event handler to invoke checkUsername:
function checkUsername() { // Send the HTTP request xhr.open("POST", "/cgi-bin/check-name-exists.pl", true); xhr.onreadystatechange = parseResponse; var username = document.forms[0].username.value; xhr.send("username=" + escape(username)); }
Unfortunately, getting to this point requires that we have already defined xhr to be an instance of our XmlHttpRequest object, which we do as follows:
function getXMLHttpRequest () { try { return new ActiveXObject("Msxml2.XMLHTTP"); } catch(e) {}; try { return new ActiveXObject("Microsoft.XMLHTTP"); } catch(e) {} try { return new XMLHttpRequest(); } catch(e) {}; return null; } var xhr = getXMLHttpRequest();
Prototype can remove much of the previous code, making it possible not only to reduce the clutter in our Web pages, but also to think at a higher level of abstraction. Just as text processing becomes easier when we think about strings rather than bits and characters, Ajax development becomes easier when we no longer need to worry about instantiating various objects correctly or keep track of their values.
We can rewrite checkUsername to take advantage of Prototype as follows:
function checkUsername() { var url = "http://www.lerner.co.il/cgi-bin/check-name-exists.pl"; var myAjax = new Ajax.Request( url, { method: 'post', parameters: $F("username"), onComplete: parseResponse }); }
In the above function, we define two variables. One of them, url, contains the URL of the server-side program to which our Ajax request will be submitted. The second variable is myAjax, which is an instance of Ajax.Request. When we create this object, we pass it our url variable, as well as an object in JSON (JavaScript Object Notation) format. This second parameter tells the new Ajax.Request object what request method and parameters to pass, as well as what function to invoke upon a successful return.
It might seem as though we have simply rewritten the original version of checkUsername. But, when you consider the changes we now can make to parseResponse, you'll see how much simpler Prototype makes our lives:
function parseResponse(originalRequest) { var response = originalRequest.responseText; var new_username = $F("username"); var warning = $("warning"); var submit_button = $("submit-button"); switch (response) { case "yes": setText(warning, "Warning: username '" + new_username +"' was taken!"); submit_button.disabled = true; break; case "no": removeText(warning); submit_button.disabled = false; break; case "": break; default: alert("Unexpected response '" + response + "'"); } }
The resulting rewrite of our program, post-ajax-register.html, is shown in Listing 5, ajax-register-prototype.html. It uses a number of features of Prototype, from simple ones, such as $(), to the Ajax request. We no longer need to wait for the response to arrive in its complete form; now we can let Prototype do the heavy lifting.
Listing 5. ajax-register-prototype.html
<!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"> <head><title>Register</title> <script type="text/javascript" src="prototype.js"></script> <script type="text/javascript"> function parseResponse(originalRequest) { var warning = $("warning"); var submit_button = $("submit-button"); switch (originalRequest.responseText) { case "yes": Element.update(warning, "Username '" + $F("username") +"' is taken!"); submit_button.disabled = true; break; case "no": Element.update(warning, ""); submit_button.disabled = false; break; case "": break; default: alert("Unexpected response '" + originalRequest.responseText + "'"); } } function checkUsername() { var url = "http://maps.lerner.co.il/cgi-bin/check-name-exists.pl"; var myAjax = new Ajax.Request( url, { method: 'get', parameters: "username=" + $F("username"), onComplete: parseResponse } ); } </script> </head> <body> <h2>Register</h2> <p id="warning"></p> <form action="/cgi-bin/register.pl" method="post" enctype="application/x-www-form-urlencoded"> <p>Username: <input type="text" id="username" name="username" onchange="checkUsername()" /></p> <p>Password: <input type="password" name="password" /></p> <p>E-mail address: <input type="text" name="email_address" /></p> <p><input type="submit" value="Register" id="submit-button" /></p> </form> </body> </html>
Several months ago, I remarked in this column that I don't very much like JavaScript. Although there still are elements of the language that I dislike, Prototype has done wonders to change my attitude toward the language. I no longer feel as bogged down in verbose syntax. Prototype has provided me with a feeling of liberation, and I'm able to concentrate on higher-level functionality rather than iterating through hierarchies of nodes or worrying about cross-browser compatibility. With a bit of practice, you also might find Prototype to be the antidote for anti-JavaScript feelings.
What's more, Prototype now sits at the base of a stack of different JavaScript libraries, such as Scriptaculous and Rico. In the coming months, we will look at what these libraries can do for your Web development, including Ajax development. We will then look at some alternatives to Prototype, which also have a great deal to offer the aspiring Ajax programmer.
Resources for this article: /article/9455.
Reuven M. Lerner, a longtime Web/database consultant, is a PhD candidate in Learning Sciences at Northwestern University in Evanston, Illinois. He currently lives with his wife and three children in Skokie, Illinois. You can read his Weblog at altneuland.lerner.co.il.