JavaScript EditorFreeware JavaScript Editor     Ajax Tutorials 



Main Page

Previous Page
Next Page

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.

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.

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.

Image from book
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.

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.

Important

Although this example is intended to be used with MySQL, you should be able to run it on other databases with little or no modification.

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 '',
  'E-mail' 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.

Note

You can download this script, along with some sample data, from www.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:

<frameset rows="100%,0" frameborder="0">
    <frame name=" displayFrame" src=" display.htm" noresize=" noresize" />
    <frame name=" hiddenFrame" src=" about:blank" noresize=" noresize" />
</frameset>

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 frameborder attribute is set to 0 to make sure 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 displayed interface.

Next up is the page to request and display the customer data. This is a relatively simple page, consisting of a text box 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 text box 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 ("txtCustomerId") from the text box. To do so, document.getElementById() is called with the text box ID, "txtCustomerId", and the value property is retrieved. (The value property holds the text that is inside the text box.) 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 this is a GET request (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:

<html>
    <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 you will read out the data and send it to the display frame. To do so, you need to 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($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['E-mail']."\">".
        $aValues['E-mail']."</a>";
    } 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 two lines clean up the database connection.

Note

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 Beginning PHP, Apache, MySQL Web Development (Wiley Press, ISBN 0-7645-5744-0).

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.

Image from book
Figure 2-2

If, on the other hand, the customer doesn't exist, 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.

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:

<frameset rows="100%,0" frameborder="0">
    <frame name="displayFrame" src=" entry.htm" noresize=" noresize" />
    <frame name="hiddenFrame" src=" about:blank" noresize=" noresize" />
</frameset>

The body of the entry form is contained within a <form/> element and has text boxes 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 = $_POST["txtName"];
    $sAddress = $_POST["txtAddress"];
    $sCity = $_POST["txtCity"];
    $sState = $_POST["txtState"];
    $sZipCode = $_POST["txtZipCode"];
    $sPhone = $_POST["txtPhone"];
    $sEmail = $_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,'E-mail') ".
              " 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.

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

<?php
    $sName = $_POST["txtName"];
    $sAddress = $_POST["txtAddress"];
    $sCity = $_POST["txtCity"];
    $sState = $_POST["txtState"];
    $sZipCode = $_POST["txtZipCode"];
    $sPhone = $_POST["txtPhone"];
    $sEmail = $_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,'E-mail') ".
              " 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($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);
?>

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 returning 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 the customer data was saved, the entry form page resembles the one shown in Figure 2-3.

Image from book
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.

Hidden iFrames

The next generation of behind-the-scenes client-server communication was to make use of iframes, 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 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" width="0" height="0"
        frameborder="0"></iframe>

Note that the iframe has its width, height, and frameborder attributes set to 0; 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);
};

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.width=0;
    oIFrameElement.height=0;
    oIFrameElement.frameBorder=0;
    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.width=0;
    oIFrameElement.height=0;
    oIFrameElement.frameBorder=0;
    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, then it calls createIFrame() and then sets a timeout to run the function again in 10 milliseconds. This is necessary because only the Internet Explorer (IE) browser 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.

Hidden IFrame POST Requests

To accomplish a POST request using hidden iframes, the 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.htm";
    }, 10);
}

This function, checkIFrame(), first checks to see if the hidden iframe has been created. If not, createIFrame() is called. Then, a timeout is set before setting the location of the iframe to ProxyForm.htm, 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.htm file is very simple. It contains only a small bit of JavaScript to notify the main page that it has been loaded:

<html>
  <head>
    <title>Proxy Form</title>
    <script type="text/javascript">

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

    </script>
  </head>
  <body>
    <form method="post"></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 oHiddenForm = oIFrame.document.forms[0];
    var oForm = document.forms[0];

    for (var i=0 ; i < oForm.elements.length; i++) {
        var oHidden = oIFrame.document.createElement("input");
        oHidden.type = "hidden";
        oHidden.name = oForm.elements[i].name;
        oHidden.value = oForm.elements[i].value;
        oHiddenForm.appendChild(oHidden);
    }

    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 (note that you must use oIFrame.document.createElement() instead of just document.createElement()). This hidden input element is assigned the name and value of the form element and added to the hidden form using the appendChild() function.

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.

Important

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.

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.

Important

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.

The 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 failed 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.

XMLHttp Requests

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 4). 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. Shortly thereafter, both the Safari (as of version 1.2) and Opera (version 7.6) browsers had duplicated Mozilla's implementation. Today, all four browsers support XMLHttp to some extent. (Safari and Opera still have incomplete implementations, supporting GET and POST but no other request types.)

Creating an XMLHttp Object

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

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

This line creates the first version of the XMLHttp 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 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

Unfortunately, the only way to determine the best 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 createXMLHttp() {
    var aVersions = [ "MSXML2.XMLHttp.5.0",
        "MSXML2.XMLHttp.4.0","MSXML2.XMLHttp.3.0",
        "MSXML2.XMLHttp","Microsoft.XMLHttp"
    ];

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

The createXMLHttp() function stores an array of XMLHttp signatures, with the most recent one first. It iterates through this array and tries to create an XMLHttp 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 XMLHttp object, an error is thrown indicating that the creation failed.

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

var oXmlHttp = new XMLHttpRequest();

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

function createXMLHttp() {

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

      for (var i = 0; i < aVersions.length; i++) {
        try {
            var oXmlHttp = new ActiveXObject(aVersions[i]);
            return oXmlHttp;
        } catch (oError) {
            //Do nothing
        }
      }
    }
    throw new Error("XMLHttp object could 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 XMLHttp object; otherwise, it checks to see if the ActiveXObject class is present and, if so, goes through the same process of creating an XMLHttp object for IE. If both of these tests fail, an error is thrown.

The other option for creating cross-browser XMLHttp 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 XMLHttp objects:

var oXmlHttp = 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.

Using XMLHttp

After you have created an XMLHttp 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 info.txt, you would start by doing this:

var oXmlHttp = zXmlHttp.createRequest();
oXmlHttp.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 XMLHttp 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 (Complete): 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. Because of differences in browser implementations, the only reliable readyState values for cross-browser development are 0, 1, and 4. In most cases, however, you will check only for 4 to see when the request has returned:

var oXmlHttp = zXmlHttp.createRequest();
oXmlHttp.open("get", "info.txt", true);
oXmlHttp.onreadystatechange = function () {
    if (oXmlHttp.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:

var oXmlHttp = zXmlHttp.createRequest();
oXmlHttp.open("get", "info.txt", true);
oXmlHttp.onreadystatechange = function () {
    if (oXmlHttp.readyState == 4) {
        alert("Got response.");
    }
};
oXmlHttp.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 XMLHttp 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 4.) So, to get the text contained in info.txt, the call would be as follows:

var sData = oXmlHttp.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 the data you've received is actually the data you want or tell the user why the data wasn't retrieved:

if (oXmlHttp.status == 200) {
    alert("Data returned is: "+ oXmlHttp.responseText;
} else {
    alert("An error occurred: "+ oXmlHttp.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.

Important

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.

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 = oXmlHttp.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 heading in the string is separated by either a new line character (\n in JavaScript) or a combination of the carriage return and new line (\r\n in JavaScript), so you can deal with individual headers as follows:

var sHeaders = oXmlHttp.getAllResponseHeaders();
var aHeaders = sHeaders.split(/\r?\n/);

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 oXmlHttp = zXmlHttp.createRequest();
oXmlHttp.open("get", "info.txt", true);
oXmlHttp.onreadystatechange = function () {
    if (oXmlHttp.readyState == 4) {
        alert("Got response.");
    }
};
oXmlHttp.setRequestHeader("myheader", "myvalue");
oXmlHttp.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.

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 oXmlHttp = zXmlHttp.createRequest();
oXmlHttp.open("get", "info.txt", false);
oXmlHttp.send(null);

if (oXmlHttp.status == 200) {
    alert("Data returned is: "+ oXmlHttp.responseText;
} else {
    alert("An error occurred: "+ oXmlHttp.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.

XMLHttp GET Requests

It's time to revisit the hidden frame GET example to see how the process could be improved using XMLHttp. 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 = "";

    $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']."<br />".$aValues['Address']."<br />".
                 $aValues['City']."<br />".$aValues['State']."<br />".
                 $aValues['Zip']."<br /><br />Phone: ".$aValues['Phone']."<br />".
                 "<a href=\" mailto:".$aValues['E-mail']."\">".
                 $aValues['E-mail']."</a>";
    } else {
        $sInfo = "Customer with ID $sID doesn't exist.";
    }

    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 XMLHttp:

function requestCustomerInfo() {
    var sId = document.getElementById("txtCustomerId").value;
    var oXmlHttp = zXmlHttp.createRequest();
    oXmlHttp.open("get", "GetCustomerData.php?id=" + sId, true);
    oXmlHttp.onreadystatechange = function () {
        if (oXmlHttp.readyState == 4) {

            if (oXmlHttp.status == 200) {
                displayCustomerInfo(oXmlHttp.responseText);
            } else {
                displayCustomerInfo("An error occurred: "+ oXmlHttp.statusText);
            }
        }
    };
    oXmlHttp.send(null);
}

Note that the function begins the same way, by retrieving the ID the user entered. Then, an XMLHttp 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), the displayCustomerInfo() function is called with the response body (accessed via responseText). If there was an error (status is not 200), 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 XMLHttp, all server errors are revealed to you as a developer, enabling you to pass along meaningful error feedback to the user. In many ways, XMLHttp is a more elegant solution than hidden frames for in-page HTTP requests.

XMLHttp POST Requests

Now that you've seen how XMLHttp 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 = $_POST["txtName"];
    $sAddress = $_POST["txtAddress"];
    $sCity = $_POST["txtCity"];
    $sState = $_POST["txtState"];
    $sZipCode = $_POST["txtZipCode"];
    $sPhone = $_POST["txtPhone"];
    $sEmail = $_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,'E-mail') ".
              " 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($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 XMLHttp 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. The getRequestBody() function handles this:

function getRequestBody(oForm) {
    var aParams = new Array();

    for (var i=0 ; i < oForm.elements.length; i++) {
        var sParam = encodeURIComponent(oForm.elements[i].name);
        sParam += "=";
        sParam += encodeURIComponent(oForm.elements[i].value);
        aParams.push(sParam);
    }

    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 and storing it in sParam, 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.

Note

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.

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

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

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

    oXmlHttp.onreadystatechange = function () {
        if (oXmlHttp.readyState == 4) {
            if (oXmlHttp.status == 200) {
                saveResult(oXmlHttp.responseText);
            } else {
                saveResult("An error occurred: "+ oXmlHttp.statusText);
            }
        }
    };
    oXmlHttp.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 XMLHttp 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.

Advantages and Disadvantages of XMLHttp

Undoubtedly, you can see the advantage of using XMLHttp 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 XMLHttp requests, so you have effectively cut off their use. It is for this reason that many Ajax applications use a mixture of XMLHttp and hidden frames to make a truly usable interface.

Another disadvantage, which applies to Internet Explorer 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, you cannot access the XMLHttp object. In that case, you may have to default to using hidden frames.


Previous Page
Next Page




JavaScript EditorAjax Editor     Ajax Validator


©