OO JavaScript and Refactoring Ajax
- To handle multiple simultaneous asynchronous requests.
- To write object-oriented JavaScript code.
- To refactor Ajax applications to make the code more reusable and easier to maintain.
JavaScript is an object-oriented programming language; however, as we'll see, its approach to objects is somewhat different than most of the other mainstream object-oriented languages. Prototype, which we have been using to handle our Ajax requests, makes use of JavaScript's object-oriented features. We have seen its Ajax object with a request() method that makes it very easy to make calls with the XMLHttpRequest object that work on most major browsers. In this lesson, we'll see why it is so useful to handle these requests in an object-oriented way. We'll start by taking a look at an example that illustrates the need to handle simultaneous requests. We'll then look at writing object-oriented JavaScript code. And we'll finish the lesson by creating our own object-oriented code for handling XMLHttpRequests.
Illustrating the Problem
The screenshot below shows a simple Ajax-based quiz.
- When the user clicks a radio button, a request is made to the server using the GET method and sending the radio button's name (e.g, "q1") and the value (e.g, 3).
- The server returns either "Right" or "Wrong" as the status text.
- A message is displayed next to the appropriate question indicating whether or not the user answered the question correctly.
That seems straightforward enough. Let's look at the code.
Code Sample: OOP/Demos/AjaxQuiz.html
<html>
<head>
<link href="AjaxQuiz.css" type="text/css" rel="stylesheet" />
<script type="text/javascript" src="Ajax.js" type="text/javascript"></script>
<script type="text/javascript">
var xmlhttp, OutputResult;
function init()
{
var quiz=document.getElementById("quizForm");
var inputs = quiz.getElementsByTagName("input");
for (i=0; i<inputs.length; i++)
{
inputs[i].onclick=CheckAnswer;
}
}
function Respond()
{
if (Math.floor(Math.random(3) * 3) == 1) //creates artificial delay 1 out of 3 times
{
setTimeout("OutputResult.innerHTML=xmlhttp.responseText",3000);
}
else
{
OutputResult.innerHTML=xmlhttp.responseText;
}
}
function SendRequest(Q,A)
{
xmlhttp = GetReq();
xmlhttp.open("GET","AjaxQuiz.jsp?q=" + Q + "&a=" + A,true);
xmlhttp.onreadystatechange=function()
{
if (xmlhttp.readyState==4 && xmlhttp.status==200)
{
Respond();
}
}
xmlhttp.send(null);
}
function CheckAnswer(e)
{
if (!e) e = window.event;
var target = e.target || e.srcElement;
var q = target.name;
var a = target.value;
OutputResult = document.getElementById(q + "Result");
OutputResult.innerHTML="checking...";
SendRequest(q,a);
}
window.onload=init;
</script>
<title>Ajax Quiz</title>
</head>
<body>
<h1>Ajax Quiz</h1>
<form onsubmit="return false" id="quizForm">
<div class="WholeQuestion">
<div class="Question">
What is 1 + 1?
</div>
<div class="Answer">
<input type="radio" name="q1" value="1" /> 1
<input type="radio" name="q1" value="2" /> 2
<input type="radio" name="q1" value="3" /> 3
</div>
<div class="Result" id="q1Result"></div>
</div>
<div class="WholeQuestion">
<div class="Question">
What is 1 + 2?
</div>
<div class="Answer">
<input type="radio" name="q2" value="1" /> 1
<input type="radio" name="q2" value="2" /> 2
<input type="radio" name="q2" value="3" /> 3
</div>
<div class="Result" id="q2Result"></div>
</div>
<div class="WholeQuestion">
<div class="Question">
What is 2 - 1?
</div>
<div class="Answer">
<input type="radio" name="q3" value="1" /> 1
<input type="radio" name="q3" value="2" /> 2
<input type="radio" name="q3" value="3" /> 3
</div>
<div class="Result" id="q3Result"></div>
</div>
</form>
</body>
</html>
And for those interested, the server-side code:
Code Sample: OOP/Demos/AjaxQuiz.jsp
<%@ page language="java" contentType="text/html; charset=ISO-8859-1" pageEncoding="ISO-8859-1"%>
<%
String question = request.getParameter("q").substring(1);
String answer = request.getParameter("a");
try
{
switch (Integer.parseInt(question)) //evaluates to an integer
{
case 1 :
if (answer.equals("2"))
{
out.write("Right");
}
else
{
out.write("Wrong");
}
break;
case 2 :
if (answer.equals("3"))
{
out.write("Right");
}
else
{
out.write("Wrong");
}
break;
case 3 :
if (answer.equals("1"))
{
out.write("Right");
}
else
{
out.write("Wrong");
}
break;
default:
out.write("Failed");
}
}
catch (Exception e)
{
out.write("Failed: " + e.toString());
}
%>
- In the first line of JavaScript code, two global variables are declared: xmlhttp and OutputResult, which will hold the XMLHttpRequest object and the div for outputting the message from the server, respectively.
- The init() function, which is called whent the page loads, attaches events to all input elements (the radio buttons) on the page, so that when they are clicked the CheckAnswer() function is called.
- CheckAnswer() gets the question and selected answer, sets OutputDiv to that answer's corresponding result div, outputs "checking..." to the newly set OutputResult div, and passes the question and answer to SendRequest().
- SendRequest() creates an XMLHttpRequest object using the GetReq() function in the Ajax.js library. It opens the request using the GET method to pass the question and selected answer to AjaxQuiz.jsp. When the server response is complete, the Respond() function is called.
- Respond() simply outputs the returned status text to the OutputResult div. It randomly creates an artificial delay. This is to demonstrate how our xmlhttp variable can get stolen by a subsequent call before the current call is complete.
And everything works beautifully. Well, almost. What happens when one of the requests or responses is delayed and, in the meantime, the user answers another question. Here's the scenario:
- The user clicks on an answer, resulting in a call to CheckAnswer() (step 3 above). This sets the global OutputResult variable to that answer's corresponding result div. It then calls SendRequest().
- SendRequest() makes the call to the server and waits for a response, which takes a long time.
- Meanwhile, our very quick user answers the next question. This calls CheckAnswer() again, which changes the OutputResult div.
If the server response were to complete at the very next moment (between the two highlighted lines above), the result of the first call would be written to the wrong result div.OutputResult = document.getElementById(q + "Result"); OutputResult.innerHTML="checking..."; SendRequest(q,a);- More than likely, however, the server response won't hit in the couple milliseconds between the time the OutputResult div is changed and SendRequest() is called, creating a brand new XMLHttpRequest object and assigning it to the same global xmlhttp variable. This means the old XMLHttpRequest object, which was still waiting for the server response, gets destroyed, leaving nobody to handle that response. The result div for that question will never get updated, being left with the text "checking..." until the user responds to that question again.
So, how do we deal with this problem? We could simply store each new request in a new variable and associate the correct result div with with that request. But we don't know how many variables to create because we don't know how many requests will be made. We could create an array of requests, appending each new request as its made. While this might do the trick for this simple example, it would get difficult to manage as the code became more complex. A better solution lies in object-oriented programming. Before we look at the solution though, we need to learn to create and manage objects in JavaScript.
Object-Oriented JavaScript
Creating new objects in JavaScript is straightforward. The code below shows how to create a simple Cat object.
Code Sample: OOP/Demos/SimpleObject.html
<html>
<head>
<script type="text/javascript">
var Cat = new Object();
Cat.name = "Socks";
Cat.color = "white";
Cat.formerOwners = new Array("Fred","Abe","Julie");
Cat.meow = function()
{
alert("Meow");
}
</script>
<title>Simple Object</title>
</head>
<body>
<a href="javascript:void(0)" onclick="Cat.meow();">Cat Sound</a>
</body>
</html>
Click on the "Cat Sound" link and the cat meows. This is a pretty useless object. Now let's see how to create a pretty useless class. Don't worry, we'll create some useful ones later on.
JavaScript Classes
A class is a template used to define the methods and properties of a particular type of object. For example, an Animal class could be used to define methods and properties common to all animals. The following example shows how to define a class and instantiate an object of that class.
Code Sample: OOP/Demos/Class.html
<html>
<head>
<script type="text/javascript">
function Animal(NAME,COLOR,OWNERS,SOUND)
{
this.name = NAME;
this.color = COLOR;
this.formerOwners = OWNERS;
this.communicate = function()
{
alert(SOUND);
}
}
var Owners = new Array("Fred","Abe","Julie");
var Cat = new Animal("Socks","white",Owners,"meow");
var Dog = new Animal("Spot","brown",Owners,"ruff");
</script>
<title>Simple Class</title>
</head>
<body>
<a href="javascript:void(0)" onclick="Cat.communicate();">Cat Sound</a>
<a href="javascript:void(0)" onclick="Dog.communicate();">Dog Sound</a>
</body>
</html>
Classes make it easy to create similar objects that vary in specific ways. Cats say "meow." Dogs say "ruff." The class constructor is simply a function that looks just like any other JavaScript function. Objects are instantiated with the keyword new followed by a call to that function. In the class definition, the keyword this is used to refer to the newly instantiated object.
The class we've created has a communicate() method, which is how we get our cats and dogs to meow and bark. Although this works fine, there is a downside to creating methods in this way: every time a new object is created, the method is recreated and stored in memory. 100 animals means 100 communicate() methods.
Prototypes
Every class, including the built-in classes, has a prototype property, which holds all the members that objects of that class will have. This way, members do not get redefined each time an object is instantiated. The example below shows how prototypes are created.
Code Sample: OOP/Demos/Prototype.html
<html>
<head>
<script type="text/javascript">
function Animal(NAME,COLOR,OWNERS,SOUND)
{
this.name = NAME;
this.color = COLOR;
this.formerOwners = OWNERS;
this.sound = SOUND;
}
Animal.prototype.communicate = function()
{
alert(this.sound);
}
var Owners = new Array("Fred","Abe","Julie");
var Cat = new Animal("Socks","white",Owners,"meow");
var Dog = new Animal("Spot","brown",Owners,"ruff");
</script>
<title>Simple Class</title>
</head>
<body>
<a href="javascript:void(0)" onclick="Cat.communicate();">Cat Sound</a>
<a href="javascript:void(0)" onclick="Dog.communicate();">Dog Sound</a>
</body>
</html>
Extending Built-in Objects
Built-in objects can be extended with prototypes. This can be a bit dangerous as it may affect code that relies on a specific set of properties in the prototype; however, it also can be useful. The code below shows how to add a display() method to the Array object. It is not a very exciting method, but it illustrates the technique.
Code Sample: OOP/Demos/ExtendingBuiltInObjects.html
<html>
<head>
<script type="text/javascript" type="text/javascript">
Array.prototype.display = function()
{
var strItems = "Array Items: \n";
for (var i=0; i < this.length; i++)
{
strItems += " * " + this[i] + "\n";
}
alert(strItems);
}
var Letters=new Array("A","B","C");
Letters.display();
</script>
<title>Extending Built-in Objects</title>
</head>
<body>
</body>
</html>
Refactoring the Ajax Request Code
Let's now return to our XMLHttpRequest calls. Earlier in this lesson, we looked at an Ajax-based quiz that uses a global variable to hold the XMLHttpRequest object. The problem with this is that that variable gets overwritten every time a new call is made, leaving any incomplete calls stranded. Now we'll take a look at an object-oriented approach to creating XMLHttpRequest calls.
The code below is based heavily on Dave Crane's ContentLoader class defined in Ajax in Action.
Code Sample: OOP/Demos/ContentLoader.js
/*
Function Name: ContentLoader
Arguments:
URL: the URL to which the request is sent
METHOD: the method used to send the request. If this is set to JSON or XML, the POST method will be used.
CALLBACK: the function to call when the server response is complete
RESULTDIV: the name of the div that will contain the output
PARAMS: an object with properties and values
Returns: Nothing
Notes: Used to create and send an XMLHttpRequest object and call a specified callback function when the server response is complete.
*/
function ContentLoader(URL,METHOD,CALLBACK,RESULTDIV,PARAMS)
{
this.xmlhttp=null;
this.url = URL;
this.method = (typeof METHOD == "undefined" || typeof METHOD == null) ? "GET" : METHOD.toUpperCase();
this.callBack=CALLBACK;
this.resultDiv = (typeof RESULTDIV == "string") ? document.getElementById(RESULTDIV) : RESULTDIV;
this.params = (typeof PARAMS == "undefined") ? null : PARAMS;
this._loadAjax();
}
ContentLoader.prototype._loadAjax=function()
{
var PostQS;
if (this.method=="POST")
{
PostQS = this._createQueryString();
}
else //GET OR HEAD
{
this.url = this.url + "?" + this._createQueryString();
PostQS = null;
}
if ( this.xmlhttp=this._createXHR() )
{
try
{
var loader=this;
this.xmlhttp.onreadystatechange=function()
{
loader._onReadyState.call(loader);
}
this.xmlhttp.open(this.method,this.url,true);
if (this.method=="POST")
{
this.xmlhttp.setRequestHeader("Content-Type","application/x-www-form-urlencoded;");
}
this.xmlhttp.send(PostQS);
}
catch(exc)
{
alert("ERROR: " + exc.message);
}
}
else
{
alert("ERROR: AJAX NOT SUPPORTED");
}
}
ContentLoader.prototype._onReadyState=function()
{
if (this.xmlhttp.readyState==4 && this.xmlhttp.status == 200)
{
if (this.callBack) this.callBack.call(this);
}
else if (this.xmlhttp.readyState==4)
{
alert("Error: " + this.xmlhttp.responseText);
}
}
/*
Function Name: _createXHR
Arguments: none
Returns: browser-specific xmlhttp object or false
*/
ContentLoader.prototype._createXHR=function()
{
try
{
xmlhttp = new XMLHttpRequest();
}
catch(exc1)
{
var ieXmlHttpVersions = new Array();
ieXmlHttpVersions[ieXmlHttpVersions.length] = "MSXML2.XMLHttp.7.0";
ieXmlHttpVersions[ieXmlHttpVersions.length] = "MSXML2.XMLHttp.6.0";
ieXmlHttpVersions[ieXmlHttpVersions.length] = "MSXML2.XMLHttp.5.0";
ieXmlHttpVersions[ieXmlHttpVersions.length] = "MSXML2.XMLHttp.4.0";
ieXmlHttpVersions[ieXmlHttpVersions.length] = "MSXML2.XMLHttp.3.0";
ieXmlHttpVersions[ieXmlHttpVersions.length] = "MSXML2.XMLHttp";
ieXmlHttpVersions[ieXmlHttpVersions.length] = "Microsoft.XMLHttp";
var i;
for (i=0; i < ieXmlHttpVersions.length; i++)
{
try
{
xmlhttp = new ActiveXObject(ieXmlHttpVersions[i]);
break;
}
catch (exc2)
{
alert(ieXmlHttpVersions[i] + " not supported.");
}
}
}
return xmlhttp;
}
ContentLoader.prototype._createQueryString=function()
{
var qs="timestamp=" + new Date().getTime();
if (typeof this.params == "object") //if no arguments are passed, arguments has a length of 1 and arguments[0] is undefined.
{
for (i in this.params)
{
qs += "&" + i + "=" + this.params[i];
}
}
return qs;
}
This ContentLoader() constructor takes five arguments:
- URL: the URL to which the request is sent.
- METHOD: the method used to send the request.
- CALLBACK: the function to call when the server response is complete.
- RESULTDIV: the name of the element that will contain the output.
- PARAMS: an object with properties and values.
The class has the following other methods :
- _loadAjax() - This is the main method. It uses the methods listed below to create and send the request.
- _onReadyState() - This method calls the callback function when the response is complete.
- _createXHR() - This method creates a browser-specific xmlhttp object.
- _createQueryString() - This method creates a querystring from the PARAMS parameter passed in.
The example below shows how the ContentLoader class abstracts all the code for making XMLHttpRequest calls into a reusable and flexible class that works across browsers.
Code Sample: OOP/Demos/UsingContentLoader.html
<html>
<head>
<title>Using ContentLoader</title>
<script src="ContentLoader.js" type="text/javascript"></script>
<script type="text/javascript">
function SayHi(FNAME,LNAME) //Passes values via an object (params)
{
var params = new Object();
params["FirstName"] = FNAME;
params["LastName"] = LNAME;
new ContentLoader("Demo.jsp","POST",Loaded,"Content",params);
}
function Loaded()
{
var resultDiv = this.resultDiv;
var xmlhttp = this.xmlhttp;
if (Math.floor(Math.random(3) * 3) == 1) //creates artificial delay 1 out of 3 times
{
setTimeout(function() { resultDiv.innerHTML += xmlhttp.responseText; },3000);
}
else
{
resultDiv.innerHTML += xmlhttp.responseText;
}
}
</script>
</head>
<body>
<a href="javascript:SayHi('Paul','McCartney')">Paul</a>
<a href="javascript:SayHi('John','Lennon')">John</a>
<a href="javascript:SayHi('Ringo','Starr')">Ringo</a>
<a href="javascript:SayHi('George','Harrison')">George</a>
<ol id="Content"></ol>
</body>
</html>
This file contains two JavaScript functions: SayHi(), which creates a new ContentLoader object and Loaded() the object's callback function. The callback function outputs a new list item to the Content list. An artificial delay is created randomly to simulate a delayed response. Open UsingContentLoader.html in your browser and click on the links. You will notice the delayed response items get added to the list as soon as they are complete. If we were to do this using a global variable to hold the xmlhttp object (as we saw in OOP/Demos/AjaxQuiz.html), the items would not have been added because the variable would have been overwritten by the next call.
OO JavaScript and Refactoring Ajax Conclusion
In this lesson of the Ajax tutorial you have learned to use object-oriented programming in JavaScript to handle multiple simultaneous asynchronous requests.
