2.2. Ajax Communication Techniques

Now that you understand the basics of how HTTP communication works, it's time to look into enacting such communication from within a web page. As you know, there are a lot of requests going back and forth between the browser and server while you are surfing the Web. Initially, all these requests happened because the user made an overt action that required such a step. Ajax techniques free developers from waiting for the user to make such an action, allowing you to create a call to the server at any time.

As discussed in Chapter 1, Ajax communication supports a number of different techniques. Each of these techniques has advantages and disadvantages, so it's important to understand which one to use in which situation.

2.2.1. The Hidden Frame Technique

With the introduction of HTML frames, the hidden frame technique was born. The basic idea behind this technique is to create a frameset that has a hidden frame that is used for client-server communication. You can hide a frame by setting its width or height to 0 pixels, effectively removing it from the display. Although some early browsers (such as Netscape 4) couldn't fully hide frames, often leaving thick borders, this technique still gained popularity among developers.

2.2.1.1. The Pattern

The hidden frame technique follows a very specific, four-step pattern (see Figure 2-1). The first step always begins with the visible frame, where the user is interacting with a web page. Naturally, the user is unaware that there is a hidden frame (in modern browsers, it is not rendered) and goes about interacting with the page as one typically would. At some point, the user performs an action that requires additional data from the server. When this happens, the first step in the process occurs: a JavaScript function call is made to the hidden frame. This call can be as simple as redirecting the hidden frame to another page or as complicated as posting form data. Regardless of the intricacy of the function, the result is the second step in the process: a request made to the server.

Figure 2.1. Figure 2-1

The third step in the pattern is a response received from the server. Because you are dealing with frames, this response must be another web page. This web page must contain the data requested from the server as well as some JavaScript to transfer that data to the visible frame. Typically, this is done by assigning an onload event handler in the returned web page that calls a function in the visible frame after it has been fully loaded (this is the fourth step). With the data now in the visible frame, it is up to that frame to decide what to do with the data.

2.2.1.2. Hidden Frame GET Requests

Now that the hidden frame technique has been explained, it's time to learn more about it. As with any new technique, the best way to learn is to work through an example. For this example, you'll be creating a simple lookup page where a customer service representative can look up information about a customer. Since this is the first example in the book, it is very simple: The user will enter a customer ID and receive in return information about the customer. Since this type of functionality will most often be used with a database, it is necessary to do some server-side programming as well. This example uses PHP, an excellent open source server-side language, and MySQL (available at www.mysql.org), an open source database that ties together very well with PHP.

NOTE

In PHP 5, MySQL support is disabled by default. For information on enabling MySQL support in PHP 5, visit www.php.net/mysql/.

First, before customer data can be looked up, you must have a table to contain it. You can create the customer table by using the following SQL script:

CREATE TABLE `Customers` (
  `CustomerId` int(11) NOT NULL auto_increment,
  `Name` varchar(255) NOT NULL default '',
  `Address` varchar(255) NOT NULL default '',
  `City` varchar(255) NOT NULL default '',
  `State` varchar(255) NOT NULL default '',
  `Zip` varchar(255) NOT NULL default '',
  `Phone` varchar(255) NOT NULL default '',

`Email` varchar(255) NOT NULL default '',
  PRIMARY KEY  (`CustomerId`)
) TYPE=MyISAM COMMENT='Sample Customer Data';

The most important field in this table is CustomerId, which is what you will use to look up the customer information.

You can download this script, along with some sample data, fromwww.wrox.com.

With the database table all set up, it's time to move on to the HTML code. To use the hidden frame technique, you must start with an HTML frameset, such as this:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Frameset//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd">
<html>
  <head>
    <title>Hidden Frame GET Example</title>
  </head>
  <frameset rows="100%,0" style="border: 0px">
    <frame name="displayFrame" src="DataDisplay.php" noresize="noresize" />
    <frame name="hiddenFrame" src="about:blank" noresize="noresize" />
  </frameset>
</html>

The important part of this code is the rows attribute of the <frameset/> element. By setting it to 100%,0, browsers know not to display the body of the second frame, whose name is hiddenFrame. Next, the style attribute is used to set the border to 0, ensuring that there isn't a visible border around each frame. The final important step in the frameset declaration is to set the noresize attributes on each frame so that the user can't inadvertently resize the frames and see what's in the hidden one; the contents of the hidden frame are never meant to be part of the visible interface.

Next up is the page to request and display the customer data (DataDisplay.php). This is a relatively simple page, consisting of a textbox to enter the customer ID, a button to execute the request, and a <div/> element to display the retrieved customer information:

<p>Enter customer ID number to retrieve information:</p>
<p>Customer ID: <input type="text" id="txtCustomerId" value="" /></p>
<p><input type="button" value="Get Customer Info"
          onclick="requestCustomerInfo()" /></p>
<div id="divCustomerInfo"></div>

You'll notice that the button calls a function named requestCustomerInfo(), which interacts with the hidden frame to retrieve information. It simply takes the value in the textbox and adds it to the query string of GetCustomerData.php, creating a URL in the form of GetCustomerData.php?id=23. This URL is then assigned to the hidden frame. Here's the function:

function requestCustomerInfo() {
    var sId = document.getElementById("txtCustomerId").value;
    top.frames["hiddenFrame"].location = "GetCustomerData.php?id=" + sId;
}

The first step in this function is to retrieve the customer identification number from the textbox. To do so, document.getElementById() is called with the textbox ID, "txtCustomerId", and the value property is retrieved. (The value property holds the text that is inside the textbox.) Then, this ID is added to the string "GetCustomerData.php?id=" to create the full URL. The second line creates the URL and assigns it to the hidden frame. To get a reference to the hidden frame, you first need to access the topmost window of the browser using the top object. That object has a frames array, within which you can find the hidden frame. Since each frame is just another window object, you can set its location to the desired URL.

That's all it takes to request the information. Note that because the request is a GET (passing information in the query string), it makes the request very easy. (You'll see how to execute a POST request using the hidden frame technique shortly.)

In addition to the requestCustomerInfo() function, you'll need another function to display the customer information after it is received. This function, displayCustomerInfo(), will be called by the hidden frame when it returns with data. The sole argument is a string containing the customer data to be displayed:

function displayCustomerInfo(sText) {
    var divCustomerInfo = document.getElementById("divCustomerInfo");
    divCustomerInfo.innerHTML = sText;
}

In this function, the first line retrieves a reference to the <div/> element that will display the data. In the second line, the customer info string (sText) is assigned into the innerHTML property of the <div/> element. Using innerHTML makes it possible to embed HTML into the string for formatting purposes. This completes the code for the main display page. Now it's time to create the server-side logic.

The basic code for GetCustomerData.php is a very basic HTML page with PHP code in two places:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" >
    <head>
        <title>Get Customer Data</title>
<?php

    //php code

?>
    </head>
    <body>
        <div id="divInfoToReturn"><?php echo $sInfo ?></div>
    </body>
</html>

In this page, the first PHP block will contain the logic to retrieve customer data (which is discussed shortly). The second PHP block outputs the variable $sInfo, containing customer data, into a <div/>. It is from this <div/> that the data is read and sent to the display frame. To do so, create a JavaScript function that is called when the page has loaded completely:

window.onload = function () {
    var divInfoToReturn = document.getElementById("divInfoToReturn");
    top.frames["displayFrame"].displayCustomerInfo(divInfoToReturn.innerHTML);
};

This function is assigned directly to the window.onload event handler. It first retrieves a reference to the <div/> that contains the customer information. Then, it accesses the display frame using the top.frames array and calls the displayCustomerInfo() function defined earlier, passing in the innerHTML of the <div/>. That's all the JavaScript it takes to send the information where it belongs. But how does the information get there in the first place? Some PHP code is needed to pull it out of the database.

The first step in the PHP code is to define all of the pieces of data you'll need. In this example, those pieces of data are the customer ID to look up, the $sInfo variable to return the information, and the information necessary to access the database (the database server, the database name, a user name, a password, and the SQL query string):

<?php

    $sID = $_GET["id"];
    $sInfo = "";

    $sDBServer = "your.databaser.server";
    $sDBName = "your_db_name";
    $sDBUsername = "your_db_username";
    $sDBPassword = "your_db_password";
    $sQuery = "Select * from Customers where CustomerId=".$sID;

    //More here
?>

This code begins with retrieving the id argument from the query string. PHP organizes all query string arguments into the $_GET array for easy retrieval. This id is stored in $sID and is used to create the SQL query string stored in $sQuery. The $sInfo variable is also created here and set to be an empty string. All the other variables in this code block contain information specific to your particular database configuration; you'll have to replace these with the correct values for your implementation.

Having captured the user's input and set up the foundation for the connection to the database, the next step is to invoke that database connection, execute the query, and return the results. If there is a customer with the given ID, $sInfo is filled with an HTML string containing all the data, including the creation of a link for the e-mail address. If the customer ID is invalid, $sInfo is filled with an error message that will be passed back to the display frame:

<?php

    $sID = $_GET["id"];
    $sInfo = "";

    $sDBServer = "your.databaser.server";
    $sDBName = "your_db_name";
    $sDBUsername = "your_db_username";
    $sDBPassword = "your_db_password";

$sQuery = "Select * from Customers where CustomerId=".$sID;

    $oLink = mysql_connect($sDBServer,$sDBUsername,$sDBPassword);
    @mysql_select_db($sDBName) or $sInfo="Unable to open database";


    if ($sInfo == "") {
        if($oResult = mysql_query($sQuery) and mysql_num_rows($oResult) > 0) {
            $aValues = mysql_fetch_array($oResult,MYSQL_ASSOC);
            $sInfo = $aValues['Name']."<br />".$aValues['Address']."<br />".
                     $aValues['City']."<br />".$aValues['State']."<br />".
                     $aValues['Zip']."<br /><br />Phone: ".$aValues['Phone']."<br />".
                     "<a     href="mailto:".$aValues['Email']."">".
                     $aValues['Email']."</a>";
            mysql_free_result($oResult);
        } else {
            $sInfo = "Customer with ID $sID doesn't exist.";
        }
    }

    mysql_close($oLink);
?>

The first two lines in the highlighted section contain the calls to connect to a MySQL database from PHP. Following that, the mysql_query() function is called to execute the SQL query. If that function returns a result and the result has at least one row, then the code continues to get the information and store it in $sInfo; otherwise, $sInfo is filled with an error message. The last line cleans up the database connection.

It's beyond the scope of this book to explain the intricacies of PHP and MySQL programming. If you'd like to learn more, consider picking up these other Wrox titles: Beginning PHP, Apache, MySQL Web Development (Wiley 2004) or Beginning PHP5, Apache, MySQL Web Development (Wiley 2005).

One final step is necessary before moving on. The preceding code, though functional, has a major security flaw. Because the customer ID is being passed in on the query string, it is not safe to take that value and add it directly into a SQL query. What if the user passed in some additional SQL that was inserted at that point? This is what is called a SQL injection attack and is very dangerous to have in a production environment. The fix for this is simple: just make sure that customer ID is actually a number and nothing more. To do this, the PHP is_numeric() function is very useful, as it determines if a string (or any other value) represents a number:

<?php

    $sID = $_GET["id"];
    $sInfo = "";

    if (is_numeric($sID)) {
        $sDBServer = "your.databaser.server";
        $sDBName = "your_db_name";>
        $sDBUsername = "your_db_username";
        $sDBPassword = "your_db_password";
        $sQuery = "Select * from Customers where CustomerId=".$sID;

        $oLink = mysql_connect($sDBServer,$sDBUsername,$sDBPassword);

@mysql_select_db($sDBName) or $sInfo="Unable to open database";

        if ($sInfo == "") {
            if($oResult = mysql_query($sQuery) and mysql_num_rows($oResult) > 0) {
                $aValues = mysql_fetch_array($oResult,MYSQL_ASSOC);
                $sInfo = $aValues['Name']."<br />".$aValues['Address']."<br />".
                     $aValues['City']."<br />".$aValues['State']."<br />".
                     $aValues['Zip']."<br /><br />Phone: ".$aValues['Phone']."<br />".
                     "<a href="mailto:".$aValues['Email']."">".
                     $aValues['Email']."</a>";
                mysql_free_result($oResult);
            } else {
                $sInfo = "Customer with ID $sID doesn't exist.";
            }
        }

    } else {
        $sInfo = "Invalid customer ID.";
    }

    mysql_close($oLink);
?>

Adding this very simple data check avoids possible SQL injection attacks by returning an error message instead of database information.

Now when $sInfo is output into the <div/>, it will contain the appropriate information. The onload event handler reads that data out and sends it back up to the display frame. If the customer was found, the information will be displayed, as shown in Figure 2-2.

Figure 2.2. Figure 2-2

If, on the other hand, the customer doesn't exist or the ID isn't a number, an error message will be displayed in that same location on the screen. Either way, the customer service representative will have a nice user experience. This completes your first Ajax example.

This example and all of the examples in the book are also available in ASP.NET and JSP in the code download for this book, available at www.wrox.com.

2.2.1.3. Hidden Frame POST Requests

The previous example used a GET request to retrieve information from a database. This was fairly simple because the customer ID could just be appended to the URL in a query string and sent on its way. But what if you need to send a POST request? This, too, is possible using the hidden frame technique, although it takes a little extra work.

A POST request is typically sent when data needs to be sent to the server as opposed to a GET, which merely requests data from the server. Although GET requests can send extra data through the query string, some browsers can handle only up to 512KB of query string information. A POST request, on the other hand, can send up to 2GB of information, making it ideal for most uses.

Traditionally, the only way to send POST requests was to use a form with its method attribute set to post. Then, the data contained in the form was sent in a POST request to the URL specified in the action attribute. Further complicating matters was the fact that a typical form submission navigates the page to the new URL. This completely defeats the purpose of Ajax. Thankfully, there is a very easy workaround in the form of a little-known attribute called target.

The target attribute of the <form/> element is used in a similar manner to the target attribute of the <a/> element: it specifies where the navigation should occur. By setting the target attribute on a form, you effectively tell the form page to remain behind while the result of the form submission is displayed in another frame or window (in this case, a hidden frame).

To begin, define another frameset. The only difference from the previous example is that the visible frame contains an entry form for customer data:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Frameset//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd">
<html>
  <head>
    <title>Hidden Frame POST Example</title>
  </head>
  <frameset rows="100%,0" style="border: 0px">
    <frame name="displayFrame" src="DataEntry.php" noresize="noresize" />
    <frame name="hiddenFrame" src="about:blank" noresize="noresize" />
  </frameset>
</html

The body of the entry form is contained within a <form/> element and has textboxes for each of the fields stored in the database (aside from customer ID, which will be autogenerated). There is also a <div/> that is used for status messages relating to the client-server communication:

<form method="post" action="SaveCustomer.php" target="hiddenFrame">
    <p>Enter customer information to be saved:</p>
    <p>Customer Name: <input type="text" name="txtName" value="" /><br />

Address: <input type="text" name="txtAddress" value="" /><br />
    City: <input type="text" name="txtCity" value="" /><br />
    State: <input type="text" name="txtState" value="" /><br />
    Zip Code: <input type="text" name="txtZipCode" value="" /><br />
    Phone: <input type="text" name="txtPhone" value="" /><br />
    E-mail: <input type="text" name="txtEmail" value="" /></p>
    <p><input type="submit" value="Save Customer Info" /></p>
</form>
<div id="divStatus"></div>

Note also that the target of the <form/> element is set to hiddenFrame so that when the user clicks the button, the submission goes to the hidden frame.

In this example, only one JavaScript function is necessary in the main page: saveResult(). This function will be called when the hidden frame returns from saving the customer data:

function saveResult(sMessage) {
    var divStatus = document.getElementById("divStatus");
    divStatus.innerHTML = "Request completed: " + sMessage;
}

It's the responsibility of the hidden frame to pass a message to this function that will be displayed to the user. This will either be a confirmation that the information was saved or an error message explaining why it wasn't.

Next is SaveCustomer.php, the file that handles the POST request. As in the previous example, this page is set up as a simple HTML page with a combination of PHP and JavaScript code. The PHP code is used to gather the information from the request and store it in the database. Since this is a POST request, the $_POST array contains all the information that was submitted:

<?php
    $sName = mysql_real_escape_string($_POST["txtName"]);
    $sAddress = mysql_real_escape_string($_POST["txtAddress"]);
    $sCity = mysql_real_escape_string($_POST["txtCity"]);
    $sState = mysql_real_escape_string($_POST["txtState"]);
    $sZipCode = mysql_real_escape_string($_POST["txtZipCode"]);
    $sPhone = mysql_real_escape_string($_POST["txtPhone"]);
    $sEmail = mysql_real_escape_string($_POST["txtEmail"]);

    $sStatus = "";

    $sDBServer = "your.database.server";
    $sDBName = "your_db_name";
    $sDBUsername = "your_db_username";
    $sDBPassword = "your_db_password";

    $sSQL = "Insert into Customers(Name,Address,City,State,Zip,Phone,`Email`) ".
              " values ('$sName','$sAddress','$sCity','$sState', '$sZipCode'".
              ", '$sPhone', '$sEmail')";

    //more here
?>

This code snippet retrieves all the POST information about the customer; moreover, it defines a status message ($sStatus) and the required database information (same as in the previous example). The SQL statement this time is an INSERT, adding in all the retrieved information.

To protect against SQL injection attacks, each of the datum retrieved from the $_POST array is escaped using mysql_real_escape_string(), a function that inserts the necessary escape sequences to ensure a string is wholly contained as a string (for example, all apostrophes are escaped so that data containing an apostrophe doesn't break the query).

The code to execute the SQL statement is very similar to that of the previous example:

<?php
    $sName = mysql_real_escape_string($_POST["txtName"]);
    $sAddress = mysql_real_escape_string($_POST["txtAddress"]);
    $sCity = mysql_real_escape_string($_POST["txtCity"]);
    $sState = mysql_real_escape_string($_POST["txtState"]);
    $sZipCode = mysql_real_escape_string($_POST["txtZipCode"]);
    $sPhone = mysql_real_escape_string($_POST["txtPhone"]);
    $sEmail = mysql_real_escape_string($_POST["txtEmail"]);

    $sStatus = "";

    $sDBServer = "your.database.server";
    $sDBName = "your_db_name";
    $sDBUsername = "your_db_username";
    $sDBPassword = "your_db_password";

    $sSQL = "Insert into Customers(Name,Address,City,State,Zip,Phone,`Email`) ".
              " values ('$sName','$sAddress','$sCity','$sState', '$sZipCode'".
              ", '$sPhone', '$sEmail')";



    $oLink = mysql_connect($sDBServer,$sDBUsername,$sDBPassword);
    @mysql_select_db($sDBName) or $sStatus = "Unable to open database";

    if ($sStatus == "") {
        if(mysql_query($sSQL)) {
            $sStatus = "Added customer; customer ID is ".mysql_insert_id();
         } else {
            $sStatus = "An error occurred while inserting; customer not saved.";
        }
    }

    mysql_close($oLink);
?>

Here, the result of the mysql_query() function is simply an indicator that the statement was executed successfully. In that case, the $sStatus variable is filled with a message indicating that the save was successful and the customer ID assigned to the data. The mysql_insert_id() function always returns the last auto-incremented value of the most recent INSERT statement. If for some reason the statement didn't execute successfully, the $sStatus variable is filled with an error message.

The $sStatus variable is output into a JavaScript function that is run when the window loads:

<script type="text/javascript">

    window.onload = function () {
        top.frames["displayFrame"].saveResult("<?php echo $sStatus ?>");
    }

</script>

This code calls the saveResult() function defined in the display frame, passing in the value of the PHP variable $sStatus. Because this variable contains a string, you must enclose the PHP echo statement in quotation marks. When this function executes, assuming that the customer data was saved, the entry form page resembles the one shown in Figure 2-3.

Figure 2.3. Figure 2-3

After this code has executed, you are free to add more customers to the database using the same form because it never disappeared.

2.2.1.4. Hidden iFrames

The next generation of behind-the-scenes client-server communication was to make use of iframes (short for inline frames), which were introduced in HTML 4.0. Basically, an iframe is the same as a frame with the exception that it can be placed inside of a non-frameset HTML page, effectively allowing any part of a page to become a frame. The iframe technique can be applied to pages not originally created as a frameset, making it much better suited to the incremental addition of functionality; an iframe can even be created on the fly in JavaScript, allowing for simple, semantic HTML to be supplied to the browser with the enhanced Ajax functionality serving as a progressive enhancement (this is discussed shortly). Because iframes can be used and accessed in the same way as regular frames, they are ideal for Ajax communication.

There are two ways to take advantage of iframes. The easiest way is to simply embed an iframe inside of your page and use that as the hidden frame to make requests. Doing this would change the first example display page to:

<p>Enter customer ID number to retrieve information:</p>
<p>Customer ID: <input type="text" id="txtCustomerId" value="" /></p>
<p><input type="button" value="Get Customer Info"
          onclick="requestCustomerInfo()" /></p>
<div id="divCustomerInfo"></div>
<iframe src="about:blank" name="hiddenFrame" style="display: none"></iframe>

Note that the iframe has its style attribute set to "display:none"; this effectively hides it from view. Since the name of the iframe is hiddenFrame, all the JavaScript code in this page will continue to work as before. There is, however, one small change that is necessary to the GetCustomerData.php page. The JavaScript function in that page previously looked for the displayCustomerInfo() function in the frame named displayFrame. If you use this technique, there is no frame with that name, so you must update the code to use parent instead:

window.onload = function () {
    var divInfoToReturn = document.getElementById("divInfoToReturn");
    parent.displayCustomerInfo(divInfoToReturn.innerHTML);
};

When accessed inside of an iframe, the parent object points to the window (or frame) in which the iframe resides. Now this example will work just as the first example in this chapter did.

The second way to use hidden iframes is to create them dynamically using JavaScript. This can get a little bit tricky because not all browsers implement iframes in the same way, so it helps to simply go step by step in creating a hidden iframe.

The first step is easy; you create the iframe using the document.createElement() method and assign the necessary attributes:

function createIFrame() {
    var oIFrameElement = document.createElement("iframe");
    oIFrameElement.style.display = "none";
    oIFrameElement.name = "hiddenFrame";
    oIFrameElement.id = "hiddenFrame";
    document.body.appendChild(oIFrameElement);

    //more code
}

The last line of this code is very important because it adds the iframe to the document structure; an iframe that isn't added to the document can't perform requests. Also note that both the name and id attributes are set to hiddenFrame. This is necessary because some browsers access the new frame by its name and some by its id attribute.

Next, define a global variable to hold a reference to the frame object. Note that the frame object for an iframe element isn't what is returned from createElement(). In order to get this object, you must look into the frames collection. This is what will be stored in the global variable:

var oIFrame = null;

function createIFrame() {
    var oIFrameElement = document.createElement("iframe");
    oIFrameElement.style.display = "none";
    oIFrameElement.name = "hiddenFrame";
    oIFrameElement.id = "hiddenFrame";
    document.body.appendChild(oIFrameElement);

    oIFrame = frames["hiddenFrame"];
}

If you place this code into the previous iframe example, you can then make the following modifications to requestCustomerInfo():

function requestCustomerInfo() {
    if (!oIFrame) {
        createIFrame();
        setTimeout(requestCustomerInfo, 10);
        return;
    }

    var sId = document.getElementById("txtCustomerId").value;
    oIFrame.location = "GetCustomerData.php?id=" + sId;
}

With these changes, the function now checks to see if oIFrame is null or not. If it is, it calls createIFrame() and then sets a timeout to run the function again in 10 milliseconds. This is necessary because only Internet Explorer recognizes the inserted iframe immediately; most other browsers take a couple of milliseconds to recognize it and allow requests to be sent. When the function executes again, it will go on to the rest of the code, where the last line has been changed to reference the oIFrame object.

Although this technique works fairly easily with GET requests, POST requests are a different story. Only some browsers will enable you to set the target of a form to a dynamically created iframe; IE is not one of them. So, to use the hidden iframe technique with a POST request requires a bit of trickery for cross-browser compatibility.

2.2.1.5. Hidden iFrame POST Requests

To accomplish a POST request using hidden iframes, the basic approach is to load a page that contains a form into the hidden frame, populate that form with data, and then submit the form. When the visible form (the one you are actually typing into) is submitted, you need to cancel that submission and forward the information to the hidden frame. To do so, you'll need to define a function that handles the creation of the iframe and the loading of the hidden form:

function checkIFrame() {
    if (!oIFrame) {
        createIFrame();

}
    setTimeout(function () {
        oIFrame.location = "ProxyForm.php";
    }, 10);
}

This function, checkIFrame(), first checks to see if the hidden iframe has been created. If not, create IFrame() is called. Then, a timeout is set before setting the location of the iframe to ProxyForm.php, which is the hidden form page. Because this function may be called several times, it's important that this page be loaded each time the form is submitted.

The ProxyForm.php file is very simple. It contains only a small bit of JavaScript to notify the main page that it has been loaded:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" >
<head>
    <title>Proxy Form</title>
    <script type="text/javascript">
    //<![CDATA[

        window.onload = function () {
            parent.formReady();
        }

    //]]>
    </script>
</head>
<body>
    <form method="post" action="#">
    </form>
</body>
</html>

As you can see, the body of this page contains only an empty form and the head contains only an onload event handler. When the page is loaded, it calls parent.formReady() to let the main page know that it is ready to accept a request. The formReady() function is contained in the main page itself and looks like this:

function formReady() {
  var oForm = document.forms[0];
  var oHiddenForm = oIFrame.document.forms[0];

  for (var i=0 ; i < oForm.elements.length; i++) {
    var oField = oForm.elements[i];

    switch (oField.type) {

      //ignore buttons
      case "button":
      case "submit":
      case "reset":

break;

      //checkboxes/radio buttons - only return the value if the control is checked.
      case "checkbox":
      case "radio":
        if (!oField.checked) {
          break;
        }

      //text/hidden/password all return the value
      case "text":
      case "hidden":
      case "password":
        createInputField(oHiddenForm, oField.name, oField.value);
        break;

      default:
        switch(oField.tagName.toLowerCase()) {
          case "select":
            createInputField(oHiddenForm, oField.name,
                             oField.options[oField.selectedIndex].value);
            break;
          default:
            createInputField(oHiddenForm, oField.name, oField.value);
        }
      }
  }

    oHiddenForm.action = oForm.action;
    oHiddenForm.submit();
};

The first step in this function is to get a reference to the form in the hidden iframe, which you can do by accessing the document.forms collection of that frame. Because there is only one form on the page, you can safely get the first form in the collection (at index 0); this is stored in oHiddenForm. Following that, a reference to the form on the main page is saved into oForm. Next, a for loop iterates through the form elements on the main page (using the elements collection). For each form element, a new hidden input element is created in the hidden frame using the createInputField() function (defined in a moment). Since there can be many different types of form elements, this code takes into account the different ways that values are stored. Buttons are ignored, since their values are usually unimportant; checkboxes and radio buttons are included only if they are checked; textboxes are always included; select boxes are given the correct value for the selected option. The function to create the fields is defined as follows:

function createInputField(oHiddenForm, sName, sValue) {
    oHidden = oIFrame.document.createElement("input");
    oHidden.type = "hidden";
    oHidden.name = sName;
    oHidden.value = sValue;
    oHiddenForm.appendChild(oHidden);
}

This function accepts three arguments: the hidden form, the name of the input field, and the value of the input field. Then, an <input/> element is created and added into the hidden form.

After each form element has been added, the hidden form is assigned the same action as the main page form. By reading the action out of the form instead of hard-coding it, you can use formReady() on any number of pages. The last step in the function is to submit the hidden form.

The only thing left to do is to make sure the main page form doesn't submit itself in the normal way. To do this, assign an onsubmit event handler that calls checkIFrame() and returns false:

<form method="post" action="SaveCustomer.php"
      onsubmit="checkIFrame();return false">
    <p>Enter customer information to be saved:</p>
    <p>Customer Name: <input type="text" name="txtName" value="" /><br />
    Address: <input type="text" name="txtAddress" value="" /><br />
    City: <input type="text" name="txtCity" value="" /><br />
    State: <input type="text" name="txtState" value="" /><br />
    Zip Code: <input type="text" name="txtZipCode" value="" /><br />
    Phone: <input type="text" name="txtPhone" value="" /><br />
    E-mail: <input type="text" name="txtEmail" value="" /></p>
    <p><input type="submit" value="Save Customer Info" /></p>
</form>
<div id="divStatus"></div>

By returning false in this way, you are preventing the default behavior of the form (to submit itself to the server). Instead, the checkIFrame() method is called and the process of submitting to the hidden iframe begins.

With this complete, you can now use this example the same way as the hidden frame POST example; the SaveCustomer.php page handles the data and calls saveResult() in the main page when completed.

NOTE

Note that the examples in this section have been simplified in order to focus on the Ajax techniques involved. If you were to use these in a real web application, you would need to provide more user feedback, such as disabling the form while a request is being made.

2.2.1.6. Advantages and Disadvantages of Hidden Frames

Now that you have seen the powerful things that you can do using hidden frames, it's time to discuss the practicality of using them. As mentioned previously, this technique has been around for many years and is still used in many Ajax applications.

One of the biggest arguments for using hidden frames is that you can maintain the browser history and thus enable users to still use the Back and Forward buttons in the browser. Because the browser doesn't know that a hidden frame is, in fact, hidden, it keeps track of all the requests made through it. Whereas the main page of an Ajax application may not change, the changes in the hidden frame mean that the Back and Forward buttons will move through the history of that frame instead of the main page. This technique is used in both Gmail and Google Maps for this very reason.

NOTE

Be careful, because iframes don't always store browser history. Whereas IE always stores the history of iframes, Firefox does so only if the iframe was defined using HTML (that is, not created dynamically using JavaScript). Safari never stores browser history for iframes, regardless of how they are included in the page.

Hidden frames do have some disadvantages. For one, you cannot make requests outside of your own domain. Due to security restrictions in browsers, JavaScript can only interact with frames that are from the same domain. Even a page from a subdomain (such as p2p.wrox.com instead of www.wrox.com) can't be accessed.

Another downside of hidden frames is that there is very little information about what's going on behind the scenes. You are completely reliant on the proper page being returned. The examples in this section all had the same problem: If the hidden frame page fails to load, there is no notification to the user that a problem has occurred; the main page will continue to wait until the appropriate JavaScript function is called. You may be able to provide some comfort to a user by setting a timeout for a long period of time, maybe five minutes, and displaying a message if the page hasn't loaded by then, but that's just a workaround. The main problem is that you don't have enough information about the HTTP request that is happening behind the scenes. Fortunately, there is another option.

2.2.2. XMLHttp Requests (XHR)

When Microsoft Internet Explorer 5.0 introduced a rudimentary level of XML support, an ActiveX library called MSXML was also introduced (discussed at length in Chapter 6). One of the objects provided in this library quickly became very popular: XMLHttp.

The XMLHttp object was created to enable developers to initiate HTTP requests from anywhere in an application. These requests were intended to return XML, so the XMLHttp object provided an easy way to access this information in the form of an XML document. Since it was an ActiveX control, XMLHttp could be used not only in web pages but also in any Windows-based desktop application; however, its popularity on the Web has far outpaced its popularity for desktop applications.

Picking up on that popularity, Mozilla duplicated the XMLHttp functionality for use in its browsers, such as Firefox. They created a native JavaScript object, XMLHttpRequest, which closely mimicked the behavior of Microsoft's XMLHttp object. Shortly thereafter, both the Safari (as of version 1.2) and Opera (version 7.6) browsers duplicated Mozilla's implementation. Microsoft even went back and created their own native XMLHttpRequest object for Internet Explorer 7. Today, all four browsers support a native XMLHttpRequest object, commonly referred to as XHR.

2.2.2.1. Creating an XHR Object

The first step to using an XHR object is, obviously, to create one. Because Microsoft's implementation prior to Internet Explorer 7 is an ActiveX control, you must use the proprietary ActiveXObject class in JavaScript, passing in the XHR control's signature:

var oXHR = new ActiveXObject("Microsoft.XMLHttp");

This line creates the first version of the XHR object (the one shipped with IE 5.0). The problem is that there have been several new versions released with each subsequent release of the MSXML library. Each release brings with it better stability and speed, so you want to make sure that you are always using the most recent version available on the user's machine. The signatures are:

  • Microsoft.XMLHttp

  • MSXML2.XMLHttp

  • MSXML2.XMLHttp.3.0

  • MSXML2.XMLHttp.4.0

  • MSXML2.XMLHttp.5.0

  • MSXML2.XMLHttp.6.0

Windows Vista ships with version 6.0, which is the preferable version to use if able. However, those running other versions of Windows won't have this available, so Microsoft recommends using the 3.0 signature as a fallback. All other versions aren't recommended for use due to varying issues with security, stability, and availability.

Unfortunately, the only way to determine which version to use is to try to create each one. Because this is an ActiveX control, any failure to create an object will throw an error, which means that you must enclose each attempt within a try...catch block. The end result is a function such as this:

function createXHR() {
    var aVersions = [ "MSXML2.XMLHttp.6.0", "MSXML2.XMLHttp.3.0"];

    for (var i = 0; i < aVersions.length; i++) {
        try {
            var oXHR = new ActiveXObject(aVersions[i]);
            return oXHR;
        } catch (oError) {
            //Do nothing
        }
    }
    throw new Error("MSXML is not installed.");
}

The createXHR() function stores an array of valid XHR signatures, with the most recent one first. It iterates through this array and tries to create an XHR object with each signature. If the creation fails, the catch statement prevents a JavaScript error from stopping execution; then the next signature is attempted. When an object is created, it is returned. If the function completes without creating an XHR object, an error is thrown indicating that the creation failed.

Fortunately, creating an XHR object is much easier in other browsers. Mozilla Firefox, Safari, Opera, and Internet Explorer 7 all use the same code:

var oXHR = new XMLHttpRequest();

Naturally, it helps to have a cross-browser way of creating XHR objects. You can create such a function by altering the createXHR() function defined previously:

function createXHR() {

    if (typeof XMLHttpRequest != "undefined") {
        return new XMLHttpRequest();
    } else if (window.ActiveXObject) {
      var aVersions = [ "MSXML2.XMLHttp.6.0", "MSXML2.XMLHttp.3.0"];

      for (var i = 0; i < aVersions.length; i++) {
        try {
            var oXHR = new ActiveXObject(aVersions[i]);
            return oXHR;
        } catch (oError) {
            //Do nothing
        }
      }

    }
    throw new Error("XMLHttp object could not be created.");
}

Now this function first checks to see if an XMLHttpRequest class is defined (by using the typeof operator). If XMLHttpRequest is present, it is used to create the XHR object; otherwise, it checks to see if the ActiveXObject class is present and, if so, goes through the same process of creating an XHR object for IE 6 and below. If both of these tests fail, an error is thrown.

The other option for creating cross-browser XHR objects is to use a library that already has cross-browser code written. The zXml library, written by two of your authors, is one such library and is available for download at www.nczonline.net/downloads/. This library defines a single function for the creation of XHR objects:

var oXHR = zXmlHttp.createRequest();

The createRequest() function, and the zXml library itself, will be used throughout this book to aid in cross-browser handling of Ajax technologies.

2.2.2.2. Using XHR

After you have created an XHR object, you are ready to start making HTTP requests from JavaScript. The first step is to call the open() method, which initializes the object. This method accepts the following three arguments:

  • Request Type: A string indicating the request type to be made—typically, GET or POST (these are the only ones currently supported by all browsers)

  • URL: A string indicating the URL to send the request to

  • Async: A Boolean value indicating whether the request should be made asynchronously

The last argument, async, is very important because it controls how JavaScript executes the request. When set to true, the request is sent asynchronously, and JavaScript code execution continues without waiting for the response; you must use an event handler to watch for the response to the request. If async is set to false, the request is sent synchronously, and JavaScript waits for a response from the server before continuing code execution. That means if the response takes a long time, the user cannot interact with the browser until the response has completed. For this reason, best practices around the development of Ajax applications favor the use of asynchronous requests for routine data retrieval, with synchronous requests reserved for short messages sent to and from the server.

To make an asynchronous GET request to a file such as info.txt, you would start by doing this:

var oXHR = zXmlHttp.createRequest();
oXHR.open("get", "info.txt", true);

Note that the case of the first argument, the request type, is irrelevant even though technically request types are defined as all uppercase.

Next, you need to define an onreadystatechange event handler. The XHR object has a property called readyState that changes as the request goes through and the response is received. There are five possible values for readyState:

  • 0 (Uninitialized): The object has been created but the open() method hasn't been called.

  • 1 (Loading): The open() method has been called but the request hasn't been sent.

  • 2 (Loaded): The request has been sent.

  • 3 (Interactive). A partial response has been received.

  • 4 (Completed): All data has been received and the connection has been closed.

Every time the readyState property changes from one value to another, the readystatechange event fires and the onreadystatechange event handler is called.

NOTE

As a result of differences in browser implementations, the only reliable readyState value is 4. Some browsers neglect states 1 and 2 altogether, and some fire 3 multiple times until the response is complete. For these reasons, it's best to only rely on readyState 4.

The onreadystatechange event handler is typically defined as:

var oXHR = zXmlHttp.createRequest();
oXHR.open("get", "info.txt", true);
oXHR.onreadystatechange = function () {
    if (oXHR.readyState == 4) {
        alert("Got response.");
    }
};

The last step is to call the send() method, which actually sends the request. This method accepts a single argument, which is a string for the request body. If the request doesn't require a body (remember, a GET request doesn't), you must pass in null (you cannot just omit the argument):

var oXHR = zXmlHttp.createRequest();
oXHR.open("get", "info.txt", true);

oXHR.onreadystatechange = function () {
    if (oXHR.readyState == 4) {
        alert("Got response.");
    }
};
oXHR.send(null);

That's it! The request has been sent and when the response is received, an alert will be displayed. But just showing a message that the request has been received isn't very useful. The true power of XHR is that you have access to the returned data, the response status, and the response headers.

To retrieve the data returned from the request, you can use the responseText or responseXML properties. The responseText property returns a string containing the response body, whereas the responseXML property is an XML document object used only if the data returned has a content type of text/xml. (XML documents are discussed in Chapter 6.) So, to get the text contained in info.txt, the call would be as follows:

var sData = oXHR.responseText;

Note that this will return the text in info.txt only if the file was found and no errors occurred. If, for example, info.txt didn't exist, then the responseText would contain the server's 404 message. Fortunately, there is a way to determine if any errors occurred.

The status property contains the HTTP status code sent in the response, and statusText contains the text description of the status (such as "OK" or "Not Found"). Using these two properties, you can make sure that the data you've received is actually the data you want or tell the user why the data wasn't retrieved:

if (oXHR.status == 200) {
    alert("Data returned is: " + oXHR.responseText);
} else {
    alert("An error occurred: " + oXHR.statusText);
}

Generally, you should always ensure that the status of a response is 200, indicating that the request was completely successful. The readyState property is set to 4 even if a server error occurred, so just checking that is not enough. In this example, the responseText property is shown only if the status is 200; otherwise, the error message is displayed.

NOTE

The statusText property isn't implemented in Opera and sometimes returns an inaccurate description in other browsers. You should never rely on statusText alone to determine if an error occurred.

Another thing to watch out for is browser caching. You may end up with a status code of 304 on a response in IE and Opera. If you are going to be accessing data that won't be changing frequently, you may want to alter your code to also check for a 304:

if (oXHR.status == 200 || oXHR.status == 304) {
    alert("Data returned is: " + oXHR.responseText);

} else {
    alert("An error occurred: " + oXHR.statusText);
}

If a 304 is returned, the responseText and responseXML properties will still contain the correct data. The only difference is that data comes from the browser's cache instead of from the server. Caching issues are discussed later in the chapter.

As mentioned previously, it's also possible to access the response headers. You can retrieve a specific header value using the getResponseHeader() method and passing in the name of the header that you want to retrieve. One of the most useful response headers is Content-Type, which tells you the type of data being sent:

var sContentType = oXHR.getResponseHeader("Content-Type");
if (sContentType == "text/xml") {
    alert("XML content received.");
} else if (sContentType == "text/plain") {
    alert("Plain text content received.");
} else {
    alert("Unexpected content received.");
}

This code snippet checks the content type of the response and displays an alert indicating the type of data returned. Typically, you will receive only XML data (content type of text/xml) or plain text (content type of text/plain) from the server, because these content types are the easiest to work with using JavaScript.

If you'd prefer to see all headers returned from the server, you can use the getAllResponseHeaders() method, which simply returns a string containing all of the headers. Each header in the string is separated by either a new line character ( ) or a combination of the carriage return and new line ( ), so you can deal with individual headers as follows:

var sHeaders = oXHR.getAllResponseHeaders();
var aHeaders = sHeaders.split(/
?
/);

for (var i=0; i < aHeaders.length; i++) {
    alert(aHeaders[i]);
}

This example splits the header string into an array of headers by using the JavaScript split() method for strings and passing in a regular expression (which matches either a carriage return/new line couple or just a new line). Now you can iterate through the headers and do with them as you please. Keep in mind that each string in aHeaders is in the format headername: headervalue.

It's also possible to set headers on the request before it's sent out. You may want to indicate the content type of data that you'll be sending, or you may just want to send along some extra data that the server may need to deal with the request. To do so, use the setRequestHeader() method before calling send():

var oXHR = zXmlHttp.createRequest();
oXHR.open("get", "info.txt", true);

oXHR.onreadystatechange = function () {
    if (oXHR.readyState == 4) {
        alert("Got response.");
    }
};
oXHR.setRequestHeader("myheader", "myvalue");
oXHR.send(null);

In this code, a header named myheader is added to the request before it's sent out. The header will be added to the default headers as myheader: myvalue.

2.2.2.3. Synchronous Requests

Up to this point, you've been dealing with asynchronous requests, which are preferable in most situations. Sending synchronous requests means that you don't need to assign theonreadystatechange event handler, because the response will have been received by the time the send() method returns. This makes it possible to do something like this:

var oXHR = zXmlHttp.createRequest();
oXHR.open("get", "info.txt", false);
oXHR.send(null);

if (oXHR.status == 200) {
    alert("Data returned is: " + oXHR.responseText);
} else {
    alert("An error occurred: " + oXHR.statusText);
}

Sending the request synchronously (setting the third argument of open() to false) enables you to start evaluating the response immediately after the call to send(). This can be useful if you want the user interaction to wait for a response or if you're expecting to receive only a very small amount of data (for example, less than 1K). In the case of average or larger amounts of data, it's best to use an asynchronous call.

NOTE

There is a chance that a synchronous call will never return. For instance, if the server process is long-running, perhaps due to an infinite loop or distributed data lookup, this could lock the entire web browser (including other tabs) for a long period of time.

2.2.2.4. XHR GET Requests

It's time to revisit the hidden frame GET example to see how the process could be improved using XHR. The first change will be to GetCustomerData.php, which must be changed from an HTML page to simply return an HTML snippet. The entire file now becomes streamlined:

<?php
    header("Content-Type: text/plain");

    $sID = $_GET["id"];
    $sInfo = "";

    if (is_numeric($sID)) {

$sDBServer = "your.databaser.server";
        $sDBName = "your_db_name";
        $sDBUsername = "your_db_username";
        $sDBPassword = "your_db_password";
        $sQuery = "Select * from Customers where CustomerId=".$sID;

        $oLink = mysql_connect($sDBServer,$sDBUsername,$sDBPassword);
        @mysql_select_db($sDBName) or $sInfo="Unable to open database";

        if ($sInfo == "") {
            if($oResult = mysql_query($sQuery) and mysql_num_rows($oResult) > 0) {
                $aValues = mysql_fetch_array($oResult,MYSQL_ASSOC);
                $sInfo = $aValues['Name']."<br />".$aValues['Address']."<br />".
                     $aValues['City']."<br />".$aValues['State']."<br />".
                     $aValues['Zip']."<br /><br />Phone: ".$aValues['Phone']."<br />".
                     "<a     href="mailto:".$aValues['Email']."">".
                     $aValues['Email']."</a>";
                mysql_free_result($oResult);
            } else {
                $sInfo = "Customer with ID $sID doesn't exist.";
            }
        }
    } else {
        $sInfo = "Invalid customer ID.";
    }

    mysql_close($oLink);

    echo $sInfo;
?>

As you can see, there are no visible HTML or JavaScript calls in the page. All the main logic remains the same, but there are two additional lines of PHP code. The first occurs at the beginning, where the header() function is used to set the content type of the page. Even though the page will return an HTML snippet, it's fine to set the content type as text/plain, because it's not a complete HTML page (and therefore wouldn't validate as HTML). You should always set the content type in any page that is sending non-HTML to the browser. The second added line is towards the bottom, where the $sInfo variable is output to the stream by using the echo command.

In the main HTML page, the basic setup is this:

<p>Enter customer ID number to retrieve information:</p>
<p>Customer ID: <input type="text" id="txtCustomerId" value="" /></p>
<p><input type="button" value="Get Customer Info"
          onclick="requestCustomerInfo()" /></p>
<div id="divCustomerInfo"></div>

The requestCustomerInfo() function previously created a hidden iframe but now must be changed to use XHR:

function requestCustomerInfo() {
    var sId = document.getElementById("txtCustomerId").value;

var oXHR = zXmlHttp.createRequest();
    oXHR.open("get", "GetCustomerData.php?id=" + sId, true);
    oXHR.onreadystatechange = function () {
        if (oXHR.readyState == 4) {
            if (oXHR.status == 200 || oXHR.status == 304) {
                displayCustomerInfo(oXHR.responseText);
            } else {
                displayCustomerInfo("An error occurred: " + oXHR.statusText);
            }
        }
    };
    oXHR.send(null);
}

Note that the function begins the same way, by retrieving the ID the user entered. Then, an XHR object is created using the zXml library. The open() method is called, specifying an asynchronous GET request for GetCustomerData.php (which has the aforementioned ID added to its query string). Next comes the assignment of the event handler, which checks for a readyState of 4 and then checks the status of the request. If the request was successful (status of 200 or 304), the displayCustomerInfo() function is called with the response body (accessed via responseText). If there was an error (status is not 200 or 304), then the error information is passed to displayCustomerInfo().

There are several differences between this and the hidden frame/iframe example. First, no JavaScript code is required outside of the main page. This is important because any time you need to keep code in two different places there is the possibility of creating incompatibilities; in the frame-based examples, you relied on separate scripts in the display page and the hidden frames to communicate with one another. By changing GetCustomerInfo.php to return just the data you're interested in, you have eliminated potential problems with JavaScript calling between these locations. The second difference is that it's much easier to tell if there was a problem executing the request. In previous examples, there was no mechanism by which you could identify and respond to a server error in the request process. Using XHR, all server errors are revealed to you as a developer, enabling you to pass along meaningful error feedback to the user. In many ways, XHR is a more elegant solution than hidden frames for in-page HTTP requests.

2.2.2.5. XHR POST Requests

Now that you've seen how XHR can simplify GET requests, it's time to take a look at POST requests. First, you need to make the same changes to SaveCustomer.php as you did for GetCustomerInfo.php, which means you need to remove extraneous HTML and JavaScript, add the content type information, and output the text:

<?php

    header("Content-Type: text/plain");

    $sName = mysql_real_escape_string($_POST["txtName"]);
    $sAddress = mysql_real_escape_string($_POST["txtAddress"]);
    $sCity = mysql_real_escape_string($_POST["txtCity"]);
    $sState = mysql_real_escape_string($_POST["txtState"]);
    $sZipCode = mysql_real_escape_string($_POST["txtZipCode"]);
    $sPhone = mysql_real_escape_string($_POST["txtPhone"]);

$sEmail = mysql_real_escape_string($_POST["txtEmail"]);

    $sStatus = "";

    $sDBServer = "your.database.server";
    $sDBName = "your_db_name";
    $sDBUsername = "your_db_username";
    $sDBPassword = "your_db_password";

    $sSQL = "Insert into Customers(Name,Address,City,State,Zip,Phone,`Email`) ".
              " values ('$sName','$sAddress','$sCity','$sState', '$sZipCode'".
              ", '$sPhone', '$sEmail')";

    $oLink = mysql_connect($sDBServer,$sDBUsername,$sDBPassword);
    @mysql_select_db($sDBName) or $sStatus = "Unable to open database";

    if ($sStatus == "") {
        if($oResult = mysql_query($sSQL)) {
            $sStatus = "Added customer; customer ID is ".mysql_insert_id();
    } else {
            $sStatus = "An error occurred while inserting; customer not saved.";
        }
    }

    mysql_close($oLink);

    echo $sStatus;
?>

This now represents the entirety of SaveCustomer.php. Note that the header() function is called to set the content type, and echo is used to output $sStatus.

In the main page, the simple form that was set up to allow entry of new customer info is the following:

<form method="post" action="SaveCustomer.php"
      onsubmit="sendRequest(); return false">
    <p>Enter customer information to be saved:</p>
    <p>Customer Name: <input type="text" name="txtName" value="" /><br />
    Address: <input type="text" name="txtAddress" value="" /><br />
    City: <input type="text" name="txtCity" value="" /><br />
    State: <input type="text" name="txtState" value="" /><br />
    Zip Code: <input type="text" name="txtZipCode" value="" /><br />
    Phone: <input type="text" name="txtPhone" value="" /><br />
    E-mail: <input type="text" name="txtEmail" value="" /></p>
    <p><input type="submit" value="Save Customer Info" /></p>
</form>
<div id="divStatus"></div>

You'll note that the onsubmit event handler has now changed to call the function sendRequest() (although the event handler still returns false to prevent actual form submission). This method first assembles the data for the POST request and then creates the XHR object to send it. The data must be sent in the format as a query string:

name1=value1&name2=value2&name3=value3

Both the name and value of each parameter must be URL-encoded in order to avoid data loss during transmission. JavaScript provides a built-in function called encodeURIComponent() that can be used to perform this encoding. To create this string, you'll need to iterate over the form fields, extracting and encoding the name and value. A helper function is used to do this encoding:

function encodeNameAndValue(sName, sValue) {
    var sParam = encodeURIComponent(sName);
    sParam += "=";
    sParam += encodeURIComponent(sValue);
    return sParam;
}

The actual iteration over the form fields takes place in the getRequestBody() function:

function getRequestBody(oForm) {

    //array to hold the params
    var aParams = new Array();

    //get your reference to the form
    var oForm = document.forms[0];

    //iterate over each element in the form
    for (var i=0 ; i < oForm.elements.length; i++) {

        //get reference to the field
        var oField = oForm.elements[i];

        //different behavior based on the type of field
        switch (oField.type) {

            //buttons - we don't care
            case "button":
            case "submit":
            case "reset":
                break;

            //checkboxes/radio buttons - only return the value if the control is checked.
            case "checkbox":
            case "radio":
                if (!oField.checked) {
                    break;
                }

            //text/hidden/password all return the value
            case "text":
            case "hidden":
            case "password":
                aParams.push(encodeNameAndValue(oField.name, oField.value));
                break;

            //everything else
            default:

                switch(oField.tagName.toLowerCase()) {

case "select":
                        aParams.push(encodeNameAndValue(oField.name,
                                      oField.options[oField.selectedIndex].value));
                        break;
                    default:
                        aParams.push(encodeNameAndValue(oField.name,
                                                        oField.value));
                }
        }

    }

    return aParams.join("&");
}

This function assumes that you will supply a reference to the form as an argument. An array (aParams) is created to store each individual name-value pair. Then, the elements of the form are iterated over, building up a string using encodeNameAndValue(), which is then added to the array. Doing this prevents multiple string concatenation, which can lead to slower code execution in some browsers. The last step is to call join() on the array, passing in the ampersand character. This effectively combines all the name-value pairs with ampersands, creating a single string in the correct format.

String concatenation in most browsers is an expensive process because strings are immutable, meaning that once created, they cannot have their values changed. Thus, concatenating two strings involves first allocating a new string and then copying the contents of the two other strings into it. Repeating this process over and over causes a severe slowdown. For this reason, it's always best to keep string concatenations at a minimum and use the array's join() method to handle longer string concatenation. Firefox actually has very efficient string concatenation, but for the purposes of cross-browser coding, it's still best to use an array and the join() method.

The sendRequest() function calls getRequestBody() and sets up the request:

function sendRequest() {
    var oForm = document.forms[0];
    var sBody = getRequestBody(oForm);

    var oXHR = zXmlHttp.createRequest();
    oXHR.open("post", oForm.action, true);
    oXHR.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");

    oXHR.onreadystatechange = function () {
        if (oXHR.readyState == 4) {
            if (oXHR.status == 200) {
                saveResult(oXHR.responseText);
            } else {
                saveResult("An error occurred: " + oXHR.statusText);
            }
        }
    };
    oXHR.send(sBody);
}

As with previous examples, the first step in this function is to get a reference to the form and store it in a variable (oForm). Then, the request body is generated and stored in sBody. Next comes the creation and setup of the XHR object. Note that the first argument of open() is now post instead of get, and the second is set to oForm.action (once again, so this script can be used on multiple pages). You'll also notice that a request header is being set. When a form is posted from the browser to a server, it sets the content type of the request as application/x-www-form-urlencoded. Most server-side languages look for this encoding in order to parse the incoming POST data properly, so it is very important for it to be set.

The onreadystatechange event handler is very similar to that of the GET example; the only change is the call to saveResult() instead of displayCustomerInfo(). The last line is very important, as the sBody string is passed to send() so that it will become part of the request body. This effectively mimics what the browser does, so all server-side logic should work as expected.

2.2.2.6. Advantages and Disadvantages of XHR

Undoubtedly, you can see the advantage of using XHR for client-server communication instead of hidden frames. The code you write is much cleaner and the intent of the code is much more apparent than using numerous callback functions with hidden frames. You have access to request and response headers as well as HTTP status codes, enabling you to determine if your request was successful.

The downside is that, unlike hidden frames, there is no browser history record of the calls that were made. The Back and Forward buttons do not tie in to XHR requests, so you have effectively cut off their use. It is for this reason that many Ajax applications use a mixture of XHR and hidden frames to make a truly usable interface.

Another disadvantage, which applies to Internet Explorer 6 and earlier only, is that you depend on ActiveX controls being enabled. If the user has your page set up in a particular security zone that doesn't allow ActiveX controls, the code cannot access the XHR object. In that case, you may have to default to using hidden frames.

It is also worth noting that XHR has the same restrictions as hidden frames when it comes to cross-domain communication. Even XMLHttp was designed for making ad hoc requests from JavaScript, it still doesn't break the cross-domain scripting rules. An XHR object is still only allowed to access resources from the same domain. If you need to access a URL located in a different origin, you must create a server-side proxy to handle the communication (see Figure 2-4).

Figure 2.4. Figure 2-4

Using a server-side proxy, the browser makes a request to the web server. The web server then contacts another web server outside of the domain to request the appropriate information. When your web server receives the response, it is forwarded back to the browser. The result is a seamless transmission of external data. You'll be using server-side proxies later in this book.

2.2.3. Ajax with Images

Since Netscape Navigator 3, it has been possible to change the src attribute of an image using JavaScript. Changing this attribute actually sends a request to the server for the image, allowing an opportunity to return data to the client. Clearly, the data is sometimes simply what is stored in the image, but there is a much greater capability for client-server communication using images.

2.2.3.1. Dynamically Creating Images

The basic technique behind using images for Ajax communication is similar to preloading images. You need to create a <img/> element and then assign its src attribute. To tell when the image is loaded, assign an onload event handler:

var oImg = document.createElement("img");
oImg.onload = function () {
  alert("Image is ready");
}
oImg.src = "/path/to/myimage.gif";

The downloading of an image begins as soon as the src attribute is assigned, meaning that you don't even need to add the image to the page. In fact, you don't even need to use an <img/> element at all; you can use the Image object:

var oImg = new Image();
oImg.onload = function () {
  alert("Image is ready");
}
oImg.src = "/path/to/myimage.gif";

There is also an error event that you can use to determine when something has gone wrong. This is most often fired when something other than an image has been returned from the server (such as HTML):

var oImg = new Image();
oImg.onload = function () {
  alert("Image is ready");
}
oImg.onerror = function () {
  alert("Error!");
};
oImg.src = "/path/to/myimage.gif";

These two events, load and error, give enough information to be a reliable communication medium from client to server. Imagine that instead of changing the src to point to another image, you have it point to a PHP, ASP.NET, or JSP page. That page can do any amount of processing that you'd like so long as it returns an image at the end. You can easily send small bits of information on the query string of the page, such as:

var oImg = new Image();
oImg.onload = function () {
  alert("Image is ready");
}
oImg.onerror = function () {
  alert("Error!");
};
oImg.src = "/path/to/myimage.php?message=ok";

Once again, as long as myimage.php returns an image, everything will behave as expected. You can return an image in one of two ways:

  • redirecting to an image

  • writing an image to the output stream

2.2.3.2. Redirecting to an Image

To redirect to an image with PHP, you need to first set the appropriate content type and then use the Location header:

<?php
    header("Content-type: image/gif");
    header("Location: pixel.gif");
?>

This example uses a GIF image. If you are redirecting to a JPEG image, you need to set the content type to image/jpeg.

In ASP.NET, you need only do a redirect:

<%@ Page Language="C#" %>
<script runat="server">
    private void Page_Load(object sender, System.EventArgs e)
    {
        Response.Redirect("pixel.gif");
    }
</script>

And in JSP:

<%
    response.sendRedirect("pixel.gif");
%>

Note that this redirect to an image, regardless of your server-side language, should be done after other processing has occurred.

2.2.3.3. Creating an Image

To output an image using PHP, you'll need to use the GD library (which is included in most PHP installations):

<?php
    header("Content-type: image/jpeg");

$image = imagecreate(1,1);
    $white = imagecolorallocate($image, 255, 255, 255);
    imagejpeg($image);
    imagedestroy($image);
?>

The first command outputs the content type for a JPEG image (GIF image creation/manipulation isn't supported in all version of GD, so it's best to use another image format). After that, a 1x1 image is created and has white allocated as a color on it. The imagejpeg() function outputs the image to the response stream and imagedestroy() frees up the memory it used.

To create and output an image using .NET, you'll need to use the System.Drawing and System.Drawing.Imaging namespaces:

<%@ Page Language="C#" ContentType="image/jpeg"%>
<%@ Import Namespace="System.Drawing" %>
<%@ Import Namespace="System.Drawing.Imaging" %>
<script runat="server">
    private void Page_Load(object sender, System.EventArgs e)
    {
        Bitmap image = new Bitmap(1, 1);
        image.Save(Response.OutputStream, System.Drawing.Imaging.ImageFormat.Jpeg);
        image.Dispose();
    }
</script>

This code mimics the previous PHP code, setting the content-type for a JPEG image, then creating a 1x1 image and outputting it to the response stream. Lastly, the image's memory is freed by calling the dispose() method.

Dynamically creating images is very similar in JSP:

<%@page contentType="image/jpeg"%>
<%@page import="java.awt.*" %>
<%@page import="java.awt.image.*" %>
<%@page import="com.sun.image.codec.jpeg.*"%>
<%
    BufferedImage image = new BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB);

    Graphics2D g = (Graphics2D) image.getGraphics();
    g.setColor(Color.white);
    g.fillRect(0,0,1,1);
    g.dispose();

    ServletOutputStream output = response.getOutputStream();
    JPEGImageEncoder encoder = JPEGCodec.createJPEGEncoder(output);
    encoder.encode(image);
%>

Here, the Java AWT library is used with the Sun JPEG codec. First, a BufferedImage is created to draw upon. Next, the graphics interface is extracted, and the image is filled with white. The last step is to output the image using a JPEGImageEncoder.

From these examples, it's easy to see that creating images dynamically is fairly straightforward, regardless of your server-side language preference.

As with redirecting to an image, the creation of an image should be done after all other processing has taken place.

Creating images dynamically or redirecting to an image provides the beginnings of Ajax communication, since both are requests that send data to the server. But this is only one part of the equation; the second part is returning data back to the client.

2.2.3.4. Images and Cookies

When people think about cookies in this age of cyber threats, most think about security concerns, spyware, and evil corporations tracking their every move. Certainly, those fears are warranted given what goes on in the world of the Web, but cookies really are just small pieces of data that can be accessed by both the client (through JavaScript) and the server. There are also several restrictions placed on cookies that you need to be aware of before using them:

  • Each domain can store a maximum of 20 cookies on a user's machine. It's best to reuse cookies whenever possible instead of creating new ones.

  • The total size of the cookie (including name, equals sign, and value) cannot exceed 4096 bytes (512 characters), meaning cookies are useful for storing small amounts of data only.

  • The total number of cookies allowed on a machine is 300.

Each request to and response from the server contains cookie information, regardless of what type of resource is being requested. This means that setting the src attribute of an image brings back updated cookie information from the server. Assuming that an onload event handler has been assigned to the image, this is where you can retrieve the new cookie information. The following function can be used to access specific cookie values:

function getCookie(sName) {
    var sRE = "(?:; )?" + encodeURIComponent(sName) + "=([^;]*);?";
    var oRE = new RegExp(sRE);

    if (oRE.test(document.cookie)) {
      return decodeURIComponent(RegExp["$1"]);
    } else {
      return null;
    }
}

This function looks through the document.cookie property, which is a series of name-value pairs representing each cookie accessible by the page. The pairs are URL-encoded and separated by semicolons, which is why a regular expression is used to extract the appropriate value. If the cookie with the given name doesn't exist, the function returns null.

It is also considered a best practice to delete cookies once they are no longer used. The following function deletes a cookie with a given name:

function deleteCookie(sName) {
    document.cookie = encodeURIComponent(sName) + "=0; " +
        "expires=Thu, 1 Jan 1970 00:00:00 UTC; path=/";
}

Setting the expiration date of a cookie to some date that has already passed effectively removes the cookie from the client machine. This function uses January 1, 1970, in GMT format to delete the cookie (the JavaScript Date object has a toGMTString() method that can be used to get this format for any date). The path argument is important as well, as it ensures that the cookie is removed for every location on the domain, not just the current page.

It's possible to recreate an earlier example, pulling customer data from a database, using this technique. Since cookies are an insecure means of storing data, this example will only pull the customer's name. The GetCustomerData.php used with the XHR example must be updated slightly to this end:

<?php

    header("Content-Type: image/gif");

    $sID = $_GET["id"];
    $sInfo = "";

    if (is_numeric($sID)) {
        $sDBServer = "your.databaser.server";
        $sDBName = "your_db_name";
        $sDBUsername = "your_db_username";
        $sDBPassword = "your_db_password";
        $sQuery = "Select * from Customers where CustomerId=".$sID;

        $oLink = mysql_connect($sDBServer,$sDBUsername,$sDBPassword);
        @mysql_select_db($sDBName) or $sInfo="Unable to open database";

        if($oResult = mysql_query($sQuery) and mysql_num_rows($oResult) > 0) {
            $aValues = mysql_fetch_array($oResult,MYSQL_ASSOC);

            $sInfo = $aValues['Name'];
            mysql_free_result($oResult);
        } else {
            $sInfo = "Customer with ID $sID doesn't exist.";
        }
    } else {
        $sInfo = "Invalid customer ID.";
    }

    mysql_close($oLink);


    setcookie("info", $sInfo);
    header("Location: pixel.gif");
?>

Note that only four lines of code have changed from the XHR example. The first sets the content-type to be image/gif, so the browser knows to expect an image back from the server; the second retrieves only the person's name and stores it in $sInfo. The other two lines (highlighted in the example code) set a cookie named "info" to contain the value of $sInfo and then redirect to the image pixel.gif.

On the client side, make sure to include the getCookie() function defined earlier in this section. This function will be used to retrieve the data sent back from GetCustomerInfo.php. The function requestCustomerInfo(), which previously used XHR, now can be updated to use an image instead:

function requestCustomerInfo() {
    var sId = document.getElementById("txtCustomerId").value;
    var oImg = new Image();
    oImg.onload = function () {
        displayCustomerInfo(getCookie("info"));
        deleteCookie("info");
    };
    oImg.onerror = function () {
        displayCustomerInfo("An error occurred while processing the request.");
    };
    oImg.src = "GetCustomerData.php?id=" + sId;
}

In this code, an Image is created and its event handlers assigned. The same displayCustomerInfo() function used in the XHR example is called to display any message returned from the server or any error message. Lastly, the src of the image is set to GetCustomerData.aspx with an ID passed in. This will yield the same user experience as the XHR example without any cross-browser compatibility issues. It is important to mention, though, that this example works because there is only a small amount of data being returned to the server. The data comfortably fits inside of a cookie; any large strings would end up being concatenated.

NOTE

Be very careful about the type of data you assign to cookies. This information is not encrypted, and it is considered poor practice to create a cookie that contains personal information such as addresses, credit card numbers, and so on. Always delete cookies once you have retrieved data from them.

2.2.3.5. Using Image Size

Another way to indicate information to the client is through the size of an image. Suppose that you want to save some information into a database, but there's the possibility the insert won't be successful. You could set up an Ajax request using images so that an image of size 1x1 means success and an image of size 2x1 is a failure. Or perhaps the request simply needs to determine if someone is logged in or not, in which case a 1x1 image indicates the user is not logged in whereas a 2x1 image indicates the user is logged in. This technique is useful when you don't need to be returning text back to the client and only need to indicate some sort of server or request state.

To check the dimensions of the image, just use the width and height properties inside of the onload event handler:

oImg.onload = function () {
    if (this.width == 1 && this.height == 1) {
        alert("Success!");

} else {
        alert("Error!");
    }
};

Since this anonymous function is assigned to the image as an event handler, the this object points to the image itself. The image's width and height are then available (because the call returned successfully) and can be interrogated to see what information the server is sending back. Of course, this technique assumes that you are merely doing something simple such as updating a customer's name, because you are not receiving specific information from the server.

This next example sends a customer ID and a name to UpdateCustomerName.php. This information is then used to update the customer's name in the database, and an image is returned to determine if this update is successful or not. Since the user must provide a customer ID to be updated, it is entirely possible that this customer may not exist in the database, in which case an error code (specific image size) must be returned. The possible return conditions are:

  • Success: 1x1 image

  • Invalid ID: 2x1 image

  • Other error: 3x1 image

The UpdateCustomerName.php file is:

<?php

    header("Content-Type: image/jpeg");

    $sID = $_GET["id"];
    $sName = mysql_real_escape_string($_GET["name"]);

    if (is_numeric($sID)) {
        $iWidth = 1;

        $sDBServer = "your.database.server";
        $sDBName = "your_db_name";
        $sDBUsername = "your_db_username";
        $sDBPassword = "your_db_password";

        $sSQL = "Update Customers set `Name` = '$sName' where CustomerId=$sID";

        $oLink = mysql_connect($sDBServer,$sDBUsername,$sDBPassword);
        @mysql_select_db($sDBName) or $iWidth = 3;

        if ($iWidth == 1) {
            if (mysql_query($sSQL)) {
                $iWidth = (mysql_affected_rows() > 0) ? 1 : 2;
                mysql_close($oLink);
            } else {
                $iWidth = 3;
            }
        }
    } else {

$iWidth = 2;
    }

    $image = imagecreate($iWidth,1);
    $white = imagecolorallocate($image, 255, 255, 255);
    imagejpeg($image);
    imagedestroy($image);
?>

This file runs a simple SQL UPDATE statement on the customer database. The $iWidth variable determines what the width of the created image will be. If an error occurs at any time during the execution of this page, $iWidth is set to 3 to indicate the error. If, on the other hand, the ID isn't in the database, $iWidth is set to 2. This situation can occur in two different ways:

  • The ID isn't numeric, so the statement is never executed.

  • The statement executes but no rows are affected.

The very last step is to create and output the image as discussed earlier.

On the client side, you need a textbox to input a customer ID, a textbox to input a name, and a button to send the request:

<form method="post" action="UpdateCustomerName.php"
        onsubmit="sendRequest(); return false">
    <p>Enter the customer ID: <input type="text" name="txtID" value="" /></p>
    <p>New customer name: <input type="text" name="txtName" value="" /></p>
    <p><input type="submit" value="Update Customer Name" /></p>
</form>
<div id="divStatus"></div>

The sendRequest() method is responsible for sending the information and interpreting the response:

function sendRequest() {
    var oForm = document.forms[0];
    var sQueryString = "id=" + encodeURIComponent(oForm.txtID.value)
                        + "&name=" + encodeURIComponent(oForm.txtName.value);
    var oImg = new Image();
    oImg.onload = function () {
        var divStatus = document.getElementById("divStatus");
        switch(this.width) {
            case 1:
                divStatus.innerHTML = "Customer name updated successfully.";
                break;
            case 2:
                divStatus.innerHTML = "Invalid customer ID; name not updated.";
                break;
            default:
                divStatus.innerHTML = "An error occurred.";
        }
    };

    oImg.onerror = function () {
        var divStatus = document.getElementById("divStatus");

divStatus.innerHTML = "An error occurred.";
    };

    oImg.src = "UpdateCustomerName.php?" + sQueryString;
}

There's nothing very different in this function versus the earlier examples. The first step is to construct the query string for the request. Next, an image is created and event handlers are assigned to it. The onload event handler is of the most importance because it is the one that interrogates the image response to determine what message to show to the user. In this case, it makes sense to use a switch statement on the image's width so that the status message can be supplied.

It's always a good idea to assign an onerror event handler to provide as much feedback as possible to the user. For this example, the event handler just outputs a simple error message. The last step is to set the src of the image to initiate the request.

NOTE

Although you could create different image sizes for different conditions, try to refrain from making an image too big. You don't want to affect user experience while waiting for a simple status from the server.

2.2.3.6. Advantages and Disadvantages

As with the other techniques mentioned to this point, using images for Ajax communication is not the solution to every task. However, the image techniques discussed in this section do offer advantages:

  • They are supported in all modern browsers as well as some older ones (such as Netscape Navigator 4, Internet Explorer 5, and Opera 6), offering a high level of compatibility.

  • Unlike hidden frames, there is some indication as to when a request is successful and when it has failed.

  • Yet another upside to using images for Ajax is that, unlike hidden frames and XHR, images are free to access images on any server, not just the one on which the containing page resides. The ability to communicate cross-domain using images has long been used by advertisers and link tracking systems to capture information; you can also use this to your advantage.

There are also disadvantages to using images for Ajax communication:

  • Not the least of these disadvantages is that images can only send GET requests, so the amount of data that can be sent back to the server is limited to the length of the URL that your browser supports (2MB in most cases). When using cookies, the amount of data that can be sent back from the server is fairly limited as well (as mentioned previously, 512 characters is the maximum size of a cookie).

  • There's a possibility that images are disabled on the client.

  • You should also be aware that some users disable cookies, so it is important to always test for cookie support before relying on any cookie-based Ajax solutions.

2.2.4. Dynamic Script Loading

A little-known and little-utilized Ajax technique is called dynamic script loading. The concept is simple: create a new <script/> element and assign a JavaScript file to its src attribute to load JavaScript that isn't initially written into the page. The beginnings of this technique could be seen way back when Internet Explorer 4.0 and Netscape Navigator 4.0 ruled the web browser market. At that time, developers learned that they could use the document.write() method to write out a <script/> tag. The caveat was that this had to be done before the page was completely loaded. With the advent of the DOM, the concept could be taken to a completely new level.

The basic technique behind dynamic script loading is very easy, just create a <script/> element using the DOM createElement() method and add it to the page:

var oScript = document.createElement("script");
oScript.type = "text/javascript";
oScript.src = "/path/to/my.js";
document.body.appendChild(oScript);

Downloading and evaluation of the JavaScript file doesn't begin until the new <script/> element is actually added to the page, so it's important not to forget this step. (This is the opposite of dynamically creating an <img/> element or Image object, which automatically begins downloading once the src attribute is assigned.)

Once the download is complete, the browser interprets the JavaScript code contained within. Now the problem becomes a timing issue: how do you know when the code has finished being loaded and interpreted? Unlike the <img/> element, the <script/> element doesn't have an onload event handler, so you can't rely on the browser to tell you when the script is complete. Instead, you'll need to have a callback function that is the executed at the very end of the source file.

2.2.4.1. Simple Example

The page in this example contains a single button which, when clicked, loads a string ("Hello world!") from an external JavaScript file. This string is passed to a callback function, named callback() for simplicity, which displays the text in an alert. The HTML for this page is:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
  <head>
    <title>Dynamic Script Loading Example 1</title>
    <script type="text/javascript">
    //<![CDATA[
        function makeRequest() {
          var oScript = document.createElement("script");
          oScript.type = "text/javascript";
          oScript.src = "example1.js";
          document.body.appendChild(oScript);
        }

        function callback(sText) {
          alert("Loaded from file: " + sText);

}
    //]]>
    </script>
  </head>
  <body>
    <input type="button" value="Click Me" onclick="makeRequest()" />
  </body>
</html>

The JavaScript file example1.js contains a single line:

callback("Hello world!");

When the button is clicked, the makeRequest() function is called, initiating the dynamic script loading. Since the newly loaded script is in context of the page, it can access and call the callback() function, which can do with the returned value as it pleases. This example works in any DOM-compliant browsers (Internet Explorer 5.0+, Safari, Firefox, and Opera 7.0+).

2.2.4.2. Dynamic Example

The previous example illustrated loading data from a static file that already exists on the server. While this may occur, it's much more likely that you'll want to load dynamic data, as with examples in the previous sections. The basic technique for this is very similar to that of using images for Ajax communication: create a dynamic page (using PHP, ASP.NET, or JSP) that accepts query string arguments and outputs JavaScript code.

Among the data being passed to the dynamic page on the server should be the name of the callback function call. This is the most optimal thing to do for maintenance purposes. Imagine what would happen if you changed the name of the function in the static page or script file and forgot to change it in the dynamic file. So, in the interest of avoiding such tight coupling and the problems that accompany it, it is much safer to pass in the name of the function that should be called.

The dynamic page then has several important jobs to do. First, it must set its content type to be text/javascript so as to identify the output as JavaScript code and not HTML or some other format. Next, the page needs to pull the callback function name from the query string and then output it, passing in any relevant data.

Suppose that the request to the dynamic page looks like this:

/path/to/js.php?id=25&callback=myCallbackFunc

The file creating the JavaScript then must look similar to this:

<?php
   header("Content-type: text/javascript");
?>

var sMessage = "Hello world!";
<?php echo $_GET["callback"] ?>(sMessage);

The first part of this file sets the content type to text/javascript so that the browser recognizes it as JavaScript. Next, a JavaScript variable called sMessage is defined as a string, "Hello world!". The last line outputs the name of the callback function that was passed through the query string, followed by parentheses enclosing sMessage, effectively making it a function call. If all works as planned, the last line becomes:

myCallbackFunc(sMessage);

Taking all of this into account, it's possible to recreate the XHR example that retrieves data from the server about a specific customer. The only part that needs to change on the client side is the requestCustomerInfo() function:

function requestCustomerInfo() {
    var sId = document.getElementById("txtCustomerId").value;
    var oScript = document.createElement("script");
    oScript.type = "text/javascript";
    oScript.src = "GetCustomerData.php?id=" + sId
                   + "&callback=displayCustomerInfo";
    document.body.appendChild(oScript);
}

Note that the same displayCustomerInfo() function will be used, so its name is passed in on the query string.

The GetCustomerData.php page also must change, though only slightly, to accommodate this technique:

<?php
    header("Content-Type: text/javascript");

    $sID = $_GET["id"];
    $sCallbackFunc = $_GET["callback"];
    $sInfo = "";

    if (is_numeric($sID)) {
        $sDBServer = "your.databaser.server";
        $sDBName = "your_db_name";
        $sDBUsername = "your_db_username";
        $sDBPassword = "your_db_password";
        $sQuery = "Select * from Customers where CustomerId=".$sID;

        $oLink = mysql_connect($sDBServer,$sDBUsername,$sDBPassword);
        @mysql_select_db($sDBName) or $sInfo="Unable to open database";

        if($sInfo == "") {
            if($oResult = mysql_query($sQuery) and mysql_num_rows($oResult) > 0) {
                $aValues = mysql_fetch_array($oResult,MYSQL_ASSOC);
                $sInfo = $aValues['Name']."<br />".$aValues['Address']."<br />".
                  $aValues['City']."<br />".$aValues['State']."<br />".
                  $aValues['Zip']."<br /><br />Phone: ".$aValues['Phone']."<br />".
                  "<a href="mailto:".$aValues['Email']."">".
                  $aValues['Email']."</a>";
                mysql_free_result($oResult);

} else {
                $sInfo = "Customer with ID $sID doesn't exist.";
            }
        }
    } else {
        $sInfo = "Invalid customer ID.";
    }

    mysql_close($oLink);


    $sEncodedInfo = str_replace(""", "\"", $sInfo);
    $sEncodedInfo = str_replace("
", "\n", $sEncodedInfo);
    echo "$sCallbackFunc("$sEncodedInfo");";
?>

The first change to the code is setting the content type to text/javascript, which as previously mentioned is necessary to identify the type of content the page is outputting. Then, the callback function name has to be retrieved from the $_GET array and stored in $sCallbackFunc. The $sInfo variable is then encoded so it will be a proper JavaScript string. To do so, all the quotation marks and new line characters have to be escaped. The resulting string is stored in $sEncodedInfo and output on the last line as a literal being passed into the callback function. This is the only line that will be output by the page.

With these changes, this example acts just as the XHR version does, including all error messages and client-side behavior.

2.2.4.3. Advantages and Disadvantages

Though dynamic script loading is a quick and easy way to establish client-server communication, it does have some drawbacks.

  • For one, there is no feedback as to what is going on once the communication is initiated. If, for example, the file you are accessing doesn't exist, there is no way for you to receive a 404 error from the server. Your site or application may sit, waiting, because the callback function was never called.

  • Also, you can't send a POST request using this technique, only a GET, which limits the amount of data that you can send. This could also be a security issue: make sure that you don't send confidential information such as passwords using dynamic script loading, as this information can easily be picked up from the query string.

Dynamic script loading does offer a couple of advantages over other techniques as well.

  • First, just like using images, it is possible to access files on other servers. This can be very powerful if you are working in a multidomain environment.

  • Further, dynamic script loading offers the ability to execute an arbitrary amount of JavaScript as the result of server-side calculations. You aren't limited to simply one callback function; use as many as necessary to achieve the desired results.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset