8.5. Tabular Information and Forms
With the server side taken care of, there are three ways to proceed with developing on the client side. The first is to continue developing the way that we've been developing, hand-coding every function. Although this would give us a really good understanding of how the application works, it would take forever to develop anything useful.
The second approach is to get online and find a suitable Ajax library, download it, and proceed with developing. Currently, quite a number of them are out there, such as Sarissa and JSON (pronounced "Jason"). (However, if memory serves, Jason was the leader or the Argonauts, whereas Ajax was a hero of the Trojan War.)
The third possibility is to write our own Ajax libraryor, rather, use one that I've already written. This approach is useful for several reasons, the first being that I'll (hopefully) know exactly how the library works. The second reason is that I can dissect them in a later chapter so that we'll know exactly how they work. The final reason is that it will help to pad the page counteh, I mean, to increase the depth of these examples. Table 8-2 briefly describes the classes in the library, along with their associated methods and properties.
Table 8-2. Ajax Library ClassesName | Parent Class | Type | Description |
---|
XMLHttpRequest | | Class | Constructor | action | XMLHttpRequest | Property | GET, POST, or HEAD | asynchronous | XMLHttpRequest | Property | true or false | envelope | XMLHttpRequest | Property | SOAP envelope | readyState | XMLHttpRequest | Method | Returns the document readyState | getresponseHeader | XMLHttpRequest | Method | Returns a single HTTP response header | getAllResponseHeaders | XMLHttpRequest | Method | Returns all HTTP response headers | responseText | XMLHttpRequest | Method | Returns the SOAP response as text | responseXML | XMLHttpRequest | Method | Returns the SOAP response as an XML document | stateChangeHandler | XMLHttpRequest | Method | Dummy state change handler | setRequestHeader | XMLHttpRequest | Method | Sets an HTTP response header | removeRequestHeader | XMLHttpRequest | Method | Removes a previously set HTTP response header | Send | XMLHttpRequest | Method | Sends the XMLHttpRequest | Cache | | Class | Constructor | insert | Cache | Method | Inserts a name/value pair | retrieve | Cache | Method | Retrieves a value | purge | Cache | Method | Purges one or more name/value pairs | names | Cache | Method | Returns an array of names | XMLDocument | | Class | Constructor | Load | XMLDocument | Method | Loads an\ XML document | serialize | XMLDocument | Method | Serializes an XML document to text | DOMDocument | XMLDocument | Method | Returns an XML document | readyState | XMLDocument | Method | Returns the document readyState | setRequestHeader | XMLDocument | Method | Sets an HTTP response header | getresponseHeader | XMLDocument | Method | Returns a single HTTP response header | getAllResponseHeaders | XMLDocument | Method | Returns all HTTP response headers | setEnvelope | XMLDocument | Method | Sets the envelope for an XMLHttpRequest | selectNodes | XMLDocument | Method | Returns an array of XML nodes | SOAPEnvelope | | Class | Constructor | envelope | SOAPEnvelope | Method | SOAP envelope |
Now that the foundations of the application architecture have been covered, albeit lightly, this is a good time to see what the HTML page built upon that architecture looks like. Figure 8-6 shows what it looks like in a browser, and Listing 8-19 shows the HTML and JavaScript.
Listing 8-19. Ajax Page
<html>
<head>
<title>chapter4</title>
<link rel="stylesheet" type="text/css" href="common.css"/>
<script language="JavaScript" src="Cache.js"></script>
<script language="JavaScript" src="XMLHTTPRequest.js"></script>
<script language="JavaScript" src="XMLDocument.js"></script>
<script language="JavaScript" src="SOAPEnvelope.js"></script>
<script language="javascript">
<!-- <![CDATA[
try {var x = new DOMParser(); var _IE = false; } catch(e)
{ var _IE = true; };
var xml = new XMLDocument();
var soap = new SOAPEnvelope();
var pageName = 'Items';
var itemsXHTMLStart = '<table width="960px" border="1" cellpadding="2"
cellspacing="2"><tr class="rowHeader">
<th width="10%">Guild</th><th width="70%">Item Name</th><th>
Item Price</th></tr>';
var itemsXHTMLEnd = '</table>';
var itemsInnerXHTML = '<tr class="rowData" id="data">
<td align="center"><a href="javascript:pageLoad(\'Items\',@guild)"
xmldi="xmlDI" xmlnode="guild_name"></a></td><td align="left">
<a href="javascript:pageLoad(\'Detail\',@item)"><div id="value"
xmldi="xmlDI" xmlnode="item_name"></div></a></td>
<td class="numeric">$<span xmldi="xmlDI"
xmlnode="item_price"></span></td></tr>';
var detailXHTML = '<div><div class="rowHeader" style="position: absolute;
left: 50px; right: auto%; bottom: auto; width: 200px; top: 75px"> Guild
Name:</div><div class="rowHeader" style="position: absolute; left: 50px;
right: auto%; bottom: auto; width: 200px; top: 92px"> Item Name:</div><div
class="rowHeader" style="position: absolute; left: 50px; right: auto%;
bottom: auto; width: 200px; top: 110px"> Description:</div><div
class="rowHeader" style="position: absolute; left: 50px; right: auto%;
bottom: auto; width: 200px; top: 127px"> Price:</div><div
class="rowHeader" style="position: absolute; left: 50px; right: auto%;
bottom: auto; width: 200px; top: 144px"> Quantity:</div><div
class="rowData" style="position: absolute; left: 255px; right: auto;
bottom: auto; width: 600px; top: 75px" xmldi="xmlDI"
xmlnode="guild_name"></div><div class="rowData" style="position: absolute;
left: 255px; right: auto; bottom: auto; width: 600px; top: 92px"
xmldi="xmlDI" xmlnode="item_name"></div>
<div class="rowData" style="position: absolute; left: 255px; right: auto;
bottom: auto; width: 600px; top: 110px" xmldi="xmlDI"
xmlnode="item_description"></div><div class="rowData" style="position:
absolute; left: 255px; right: auto; bottom: auto; width: 600px; top:
127px">$<span xmldi="xmlDI" xmlnode="item_price"></span></div><input
type="text" id="quantity" name="quantity" value=""
onkeyup="restrict(this,\'[0-9]\',\'gi\')" class="rowData" style="position:
absolute; left: 255px; right: auto; bottom: auto; width: 600px; top:
144px; text-align: right"></div>';
function setEvents() {
pageLoad();
}
function pageLoad(name,parm) {
switch(true) {
case(arguments.length == 0):
soap.content = '<guild_item_id/><guild_id/>';
case(name == 'Items'):
if(arguments.length != 0)
soap.content =
'<guild_item_id/><guild_id>' + parm + '</guild_id>';
soap.operator = 'getItems';
xml.setEnvelope(soap.envelope());
xml.setRequestHeader('SOAPAction','http://tempuri.org/getItems');
xml.setRequestHeader('Content-Type','text/xml');
xml.load('http://localhost/AJAX4/chapter4.asmx');
window.setTimeout('pageWait()',10);
pageName = 'Items';
break;
case(name == 'Detail'):
soap.content =
'<guild_item_id>' + parm + '</guild_item_id><guild_id/>';
soap.operator = 'getItems';
xml.setEnvelope(soap.envelope());
xml.setRequestHeader('SOAPAction','http://tempuri.org/getItems');
xml.setRequestHeader('Content-Type','text/xml');
xml.load('http://localhost/AJAX4/chapter4.asmx');
window.setTimeout('pageWait()',10);
pageName = name;
break;
default:
alert(name);
}
}
function pageWait() {
if(xml.readyState() == 4) {
var xhtml = itemsXHTMLStart;
var input =
document.getElementById('buttons').getElementsByTagName('input');
if(_IE)
document.getElementById('xmlDI').XMLDocument.loadXML(xml.selectSingleNode(
'//NewDataSet').serialize());
else
document.getElementById('xmlDI').innerHTML =
xml.selectSingleNode('//NewDataSet').serialize();
switch(pageName) {
case('Items'):
for(var i=0;i < xml.selectNodes('//Table').length;i++) {
var reGuild = new RegExp('@guild','i');
var reItem = new RegExp('@item','i');
var guild =
xml.selectNodes('//guild_id')[i].serialize().replace(new
RegExp('<[^<]{0,}>','g'),'');
var item =
xml.selectNodes('//guild_item_id')[i].serialize().replace(new
RegExp('<[^<]{0,}>','g'),'');
xhtml +=
itemsInnerXHTML.replace(reGuild,guild).replace(reItem,item);
}
document.getElementById('formBody').innerHTML = xhtml +
itemsXHTMLEnd;
break;
case('Detail'):
document.getElementById('formBody').innerHTML = detailXHTML;
break;
}
window.setTimeout('_bind()',10);
} else
window.setTimeout('pageWait()',10);
}
function _bind() {
if(arguments.length == 0) {
doBind(document.body.getElementsByTagName('a'));
doBind(document.body.getElementsByTagName('div'));
doBind(document.body.getElementsByTagName('input'));
doBind(document.body.getElementsByTagName('select'));
doBind(document.body.getElementsByTagName('span'));
doBind(document.body.getElementsByTagName('textarea'));
} else {
applyChange(arguments[0],arguments[1]);
_bind(); // Re-bind
}
/*
Function: doBind
Programmer: Edmond Woychowsky
Purpose: To handle data-binds for specific nodes based
upon HTML element type and browser type.
*/
function doBind(objects) {
var strTag; // HTML tag
var strDI; // XML data island id
var strNode; // XML node name
var strValue; // XML node value
var index = new Object(); // Object to store information
for(var i=0;i < objects.length;i++) {
strTag = objects[i].tagName;
strDI = objects[i].getAttribute('xmldi');
strNode = objects[i].getAttribute('xmlnode');
if(strDI != null && strNode != null) {
if(typeof(index[strNode]) == 'undefined')
index[strNode] = -1;
++index[strNode];
if(_IE) {
strValue =
document.getElementById(strDI).XMLDocument.selectNodes('//' +
strNode).item(index[strNode]).text;
} else {
strValue =
document.getElementById(strDI).getElementsByTagName(strNode)[index[strNode
]].innerHTML;
}
switch(strTag) {
case('A'):
case('DIV'):
case('SPAN'):
objects[i].innerHTML = strValue;
break;
case('INPUT'):
switch(objects[i].type) {
case('text'):
case('hidden'):
case('password'):
objects[i].value = strValue;
objects[i].onchange = new Function("_bind(this," +
i.toString() + ")");
break;
case('checkbox'):
if(objects[i].value == strValue)
objects[i].checked = true;
else
objects[i].checked = false;
objects[i].onclick = new Function("_bind(this," +
i.toString() + ")");
break;
case('radio'):
if(_IE)
strValue =
document.getElementById(strDI).XMLDocument.selectNodes('//' +
strNode).item(0).text;
else
strValue =
document.getElementById(strDI).getElementsByTagName(strNode)[0].innerHTML;
if(objects[i].value == strValue)
objects[i].checked = true;
else
objects[i].checked = false;
objects[i].onclick = new
Function("_bind(this,0)");
break;
}
break;
case('SELECT'):
case('TEXTAREA'):
objects[i].value = strValue;
objects[i].onchange = new Function("_bind(this," +
i.toString() + ")");
break;
}
}
}
}
}
/*
Function: restrict
Programmer: Edmond Woychowsky
Purpose: Restrict keyboard input for the provided object
using the passed regular expression and option.
*/
function restrict(obj,rex,opt) {
var re = new RegExp(rex,opt);
var chr = obj.value.substr(obj.value.length - 1);
if(!re.test(chr)) {
var reChr = new RegExp(chr,opt);
obj.value = obj.value.replace(reChr,'');
}
}
/*
Function: add2Cart
Programmer: Edmond Woychowsky
Purpose: To add an item/quantity pair to an XML Data
Island that represents a shopping cart.
*/
function add2Cart() {
var item =
xml.selectSingleNode('//guild_item_id').serialize().replace(new
RegExp('<[^<]{0,}>','g'),'');
var quantity = document.getElementById('quantity').value;
var re = new RegExp('<item><id>' + item +
'</id><quantity>[^<]{1,}</quantity></item>','g');
if(re.test(document.getElementById('cart').innerHTML))
document.getElementById('cart').innerHTML =
document.getElementById('cart').innerHTML.replace(re,'');
document.getElementById('cart').innerHTML += '<item><id>' + item +
'</id><quantity>' + quantity + '</quantity></item>';
alert('Item added to cart.');
}
// ]]> >
</script>
</head>
<body onload="setEvents()">
<table border="0" height="60px" width="975px" cellpadding="0"
cellspacing="0" ID="Table1">
<tr class="pageHeader" height="40px">
<td width="5%"> </td>
<th id="systemName" class="pageCell" width="45%" align="left">My
System</th>
<th id="pageName" class="pageCell" width="45%" align="right">My
Page</th>
<td width="5%"> </td>
</tr>
<tr>
<td> </td>
<td> </td>
<td> </td>
<td> </td>
</tr>
</table>
<xml id="cart"></xml>
<xml id="xmlDI"></xml>
<div id="formBody" style="color: #000000; background-color: F0F8FF;
font-family: tahoma; font-size: 12px; border: solid 1px gray; height:
400px; width: 980px; overflow: scroll"></div>
<p />
<div id="buttons">
<input id="show_all" type="button" value="Show All"
onclick="javascript:pageLoad()" style="height: 22px; width: 110px" />
<input id="add_to_cart" type="button" value="Add to cart"
onclick="add2Cart()" style="height: 22px; width: 110px" />
<input id="view_cart" type="button" value="View cart"
onclick="javascript:pageLoad('displayCart')" style="height: 22px; width:
110px" />
<input id="place_order" type="button" value="Place order" onclick=""
style="height: 22px; width: 110px" />
</div>
</body>
</html>
|
Just as in the earlier HTML examples, Listing 8-19 has bound XML data islands and an asynchronous XMLHTTP request. The biggest differences are that the XML comes from a web service and that the request is made using SOAP. This means that although all the code that you see here is custom for this book, there is absolutely no reason why an Ajax front end cannot be written for existing web services. It's like General Patten said: "Never pay twice for the same real estate."
Please take note of the HTML DIV tag with the id attribute; there is something special about it. As you've probably deduced from the style attribute, both its height and its width are static. This is to keep the buttons along the bottom from moving around. In addition, it provides someplace to display the information returned from the server, without having to worry about the buttons. An alternative would be to put the buttons on the top of the page, but scrolling up to find the buttons would get old really quickly. With the underlying architecture around 90 percent complete, let's revisit the page that displays the items available for purchase on our site.
8.5.1. Read Only
Again, the purpose of the read-only page is to display our wares to visitors. On the surface, it is just rows and rows of items that are available for sale. Behind the scenes, however, is a different story. This is a web service delivering a SOAP response to a request for informationin this instance, the information relating to the items for sale.
Upon receiving the request, the web service obtains the necessary information from the database, which is the same MySQL database from the previous chapters. When it has the information, it programmatically builds the XHTML required to fill the scrollable div. Updates are not permitted on this page, so only the XHTML is being sent to the client. Hey, conserve bandwidth wherever you can.
Unfortunately, there is more to it than that. For instance, the page's onload event handler needs to send the SOAP request so that the previous method is invoked. In addition, buttons need to be activated or deactivated, clicks need to be handled, and, in short, there is more work to do.
Starting with the handler for the page onload event, we need to build a SOAP request, send the request to the web service, and activate the appropriate buttons. In addition, eventually the web service will get back to the page with its response, which will have to be dealt with. Sound like enough? Let's break it down into a little more detail.
1. | Create a global instance of XMLDocument().
| 2. | Build a SOAP request describing the URI of the web service, the method, the namespace, and the parameters being sent.
| | | 3. | Send the SOAP request using the XMLHttpRequest that is incorporated into the XMLDocument class.
| 4. | Wait for the SOAP response from the web service.
| 5. | Active the appropriate buttons.
| 6. | Populate the page.
|
Sound pretty easy? Well, it is easy, after the first time. The first time, however, it is kind of difficult to figure out what is what and what goes where. The first time that I did this, I stumbled a bit on steps 2 and 4. The problem that I had with step 2 was simply a matter of what goes where; a look at the code will explain everything. Dealing with step 4 is merely a matter of using window.setTimeout in JavaScript to repeatedly call a function after a suitable number of milliseconds to check the readyState of the XMLHttpRequest. If the readyState is 4, it is complete. Table 8-3 shows the possible readyState values and their meanings.
Table 8-3. readyState ValuesreadyState | Description |
---|
0 | Uninitialized | 1 | Loading | 2 | Loaded | 3 | Interactive | 4 | Complete |
Probably the hardest thing to get used to with Ajax is the ratio of client-side JavaScript to HTML. With traditional web development, the number of lines of HTML far exceeds the number of lines of JavaScript. With Ajax development, it is the other way around, with more JavaScript than HTML. Fortunately, with a halfway decent library of objects and functions, Ajax development doesn't usually need a lot of custom code. For example, Listing 8-20 shows the custom JavaScript for our page listing the items available, and Figure 8-7 shows what it looks like in the browser.
Listing 8-20. Items Available
soap.content =
'<guild_item_id>' + parm + '</guild_item_id><guild_id/>';
soap.operator = 'getItems';
xml.setEnvelope(soap.envelope());
xml.setRequestHeader('SOAPAction','http://tempuri.org/getItems');
xml.setRequestHeader('Content-Type','text/xml');
xml.load('http://localhost/AJAX4/chapter4.asmx');
window.setTimeout('pageWait()',10);
pageName = name;
function pageWait() {
if(xml.readyState() == 4) {
var xhtml = itemsXHTMLStart;
var input =
document.getElementById('buttons').getElementsByTagName('input');
if(_IE)
document.getElementById('xmlDI').XMLDocument.loadXML(xml.selectSingleNode(
'//NewDataSet').serialize());
else
document.getElementById('xmlDI').innerHTML =
xml.selectSingleNode('//NewDataSet').serialize();
switch(pageName) {
case('Items'):
for(var i=0;i < xml.selectNodes('//Table').length;i++) {
var reGuild = new RegExp('@guild','i');
var reItem = new RegExp('@item','i');
var guild =
xml.selectNodes('//guild_id')[i].serialize().replace(new
RegExp('<[^<]{0,}>','g'),'');
var item =
xml.selectNodes('//guild_item_id')[i].serialize().replace(new
RegExp('<[^<]{0,}>','g'),'');
xhtml +=
itemsInnerXHTML.replace(reGuild,guild).replace(reItem,item);
}
document.getElementById('formBody').innerHTML = xhtml +
itemsXHTMLEnd;
break;
case('Detail'):
document.getElementById('formBody').innerHTML =
detailXHTML;
break;
}
window.setTimeout('_bind()',10);
} else
window.setTimeout('pageWait()',10);
}
|
The pageWait() function shown here might seem somewhat formidable, but its sole purpose is to dynamically build the HTML necessary for the bound table in the page. This is a somewhat slick trick, but really nothing that hasn't been done for the last five years, although usually for different reasons.
8.5.2. Updateable
Because we've worked out the underlying architecture, an updateable page is merely a variant of the read-only page shown in the previous example. There are essentially two differences, the first being that, instead of using SPAN or DIV tags, the bound tags are things such as INPUT and SELECT. The second difference is that eventually it will be necessary to send an entire XML data island to the server. The interesting thing about this is that it doesn't have to be the XML Data Island that is bound to the HTML, although it could be.
Remember the shopping cart from earlier in the book? Well, instead of using the funky item id-dash-quantity in a text box, now the shopping is itself an XML Data Island. Unfortunately, this means that I can't be lazy and recycle the function from Chapter 5. Alas, it was necessary to write the function shown in Listing 8-21. It's not anything fancy; in fact, it treats the XML as text. Not only is that a valid option, but it also works in a cross-browser environment.
Listing 8-21. Add to Shopping Cart Function
/*
To add an item/quantity pair to an XML Data Island that
represents a shopping cart.
*/
function add2Cart() {
var item =
xml.selectSingleNode('//guild_item_id').serialize().replace(new
RegExp('<[^<]{0,}>','g'),'');
var quantity = document.getElementById('quantity').value;
var re =
new RegExp('<item><id>' + item +
'</id><quantity>[^<]{1,}</quantity></item>','g');
if(re.test(document.getElementById('cart').innerHTML))
document.getElementById('cart').innerHTML =
document.getElementById('cart').innerHTML.replace(re,'');
document.getElementById('cart').innerHTML += '<item><id>' + item +
'</id><quantity>' + quantity + '</quantity></item>';
alert('Item added to cart.');
}
|
The end result of this is the page that was shown in Listing 8-21 and Figures 8-7 and 8-8. It works roughly the same as the pageWait() function from Listing 8-20. The difference is that, instead of adding elements to the HTML document based upon an XML document, elements are added to the embedded XML document based upon the actions of the visitor. The page shown in Figure 8-7 lists the items available for purchase, and Figure 8-8 handles the add to the shopping cart.
|