Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Beginning JavaScript With DOM Scripting And Ajax - From Novice To Professional (2006)

.pdf
Скачиваний:
80
Добавлен:
17.08.2013
Размер:
17.27 Mб
Скачать

306

C H A P T E R 8 B A C K - E N D I N T E R A C T I O N W I T H A J A X

The readyState property of the request object contains a numeric value that describes what is happening to the connection. It is incremented throughout the connection attempt. The different possible values for readyState and their corresponding request states are as follows:

0: There is no connection—it is uninitialized.

1: The connection is loading.

2: The data was loaded.

3: The connection is interactive.

4: The connection is complete—the data was sent and retrieved.

Every time the status changes, XHR triggers a readystatechange event. You can use the corresponding onreadystatechange event handler to invoke a method in which you can test against the possible values of readyState and take the appropriate action.

simpleXHR.js (continued)

request.onreadystatechange = function() { if( request.readyState == 1 ) {

simplexhr.outputContainer.innerHTML = 'loading...';

}

Once the request is initialized (readyState equals 1), it is a very good idea to give the user some feedback that there are things happening in the background. In this example, the script displays a “loading...” message inside the HTML output element as shown in Figure 8-3.

Figure 8-3. Notifying the user that the request was sent and is under way

C H A P T E R 8 B A C K - E N D I N T E R A C T I O N W I T H A J A X

307

The other states cannot be read safely cross-browser, which is why we skip 2 and 3 and check whether the request was finished by comparing readyState with 4.

simpleXHR.js (continued)

if( request.readyState == 4 ) {

if ( /200|304/.test( request.status ) ) { simplexhr.retrieved(request);

}else { simplexhr.failed(request);

}

}

When the request is complete, you check another property called status, which stores the status of the request. The status is the standard HTTP response code of the request. It is 0 when the connection cannot be established and 404 when the file is not found.

Note For a complete list of standard HTTP response codes, see http://www.w3.org/Protocols/ rfc2616/rfc2616-sec10.html.

If the status is either 200 (all OK) or 304 (not modified), the file has been retrieved and you can do something with it. In the case of this demo script, you call the retrieved() method. If the status is any other value, you call failed().

simpleXHR.js (continued)

}

request.send( null ); return false;

},

The send() method sends your request to the server and can take request parameters to send to the server-side script being invoked. If you don’t have any parameters to send, it is safest to set it to null. (Internet Explorer accepts send() without any parameters, but this can cause problems in Mozilla browsers.) Finally, setting the method’s return value to false stops the link from being followed.

simpleXHR.js (continued)

failed : function( requester ) {

alert( 'The XMLHttpRequest failed. Status: ' + requester.status ); return true;

},

308

C H A P T E R 8 B A C K - E N D I N T E R A C T I O N W I T H A J A X

If the request didn’t succeed, the failed() method shows an alert() dialog telling the user about the problem. (This is not very clever or pretty, but should do for the moment.) Returning true after the user has clicked the dialog’s OK button causes the link to be followed. You can test this by opening the file exampleXHR.html locally in a browser (without the http:// protocol) and clicking the links. As there is no HTTP transmission, any request will fail with code 0 as shown in Figure 8-4.

Figure 8-4. Notifying the user that the XMLHttpRequest failed

However, if all went well with the request, the retrieved() method takes over:

simpleXHR.js (continued)

retrieved : function( requester ) { var data = requester.responseText;

data = data.replace( /\n/g, '<br />' ); simplexhr.outputContainer.innerHTML = data; return false;

}

}

This method enables you to obtain and use the data sent back from the XMLHttpRequest. The data can be read out in two different formats: responseText and responseXML, the difference between these being the type of output—responseText returns a string, and responseXML will return an XML object. You can use all the usual string properties and methods on responseText (length, indexOf(), replace(), etc.), and all the DOM methods on responseXML (getElementsByTagName(), getAttribute(), and so on).

In this example, you merely retrieve text and use the String.replace() method to convert all line breaks, \n, into BR elements. Then you can write out the changed string as innerHTML to the outputContainer and return false to stop the normal link behavior.

In many cases, it is sufficient to use responseText and write out data via innerHTML. It is also a lot quicker and less work for the user’s browser and CPU than using XML and DOM to convert the objects back into HTML.

C H A P T E R 8 B A C K - E N D I N T E R A C T I O N W I T H A J A X

309

Note The Ajax acronym doesn’t really work for these examples, as the process lacks the XML component. For this reason, this approach is known as AHAH and is defined as a microformat with code examples at http://microformats.org/wiki/rest/ahah.

Et Tu, Cache?

Normally the browser cache is our friend. The browser stores downloaded files in it, which means the user doesn’t have to download our scripts over and over again. However, in the case of Ajax, caching can cause problems.

Safari is the main offender, as it caches the response status and does not trigger the changes (remember that the status returns the HTTP code 200, 304, or 404) any longer. However, avoiding issues with caching is pretty simple: before calling the send() method, add another header to the request. This header tells the browser to test whether the data has changed since a certain date. Which date you set doesn’t matter, as long as it is in the past, for example, at the time of this writing:

request.setRequestHeader( 'If-Modified-Since', 'Thu, 06 Apr 2006 00:00:00 GMT' );

request.send( null );

Putting the X Back into Ajax

If you use responseXML, you can use DOM methods to turn the received XML into HTML. The demo exampleXMLxhr.html does this. As a data source, take the album collection used in the pagination example in the last chapter in XML format:

albums.xml (excerpt)

<?xml version="1.0" encoding="utf-8"?> <albums>

<album>

<id>1</id>

<artist>Depeche Mode</artist> <title>Playing the Angel</title>

<comment>They are back and finally up to speed again</comment> </album>

<album>

<id>2</id>

<artist>Monty Python</artist> <title>The final Rip-Off</title>

<comment>Double CD with all the songs</comment> </album>

[... more albums snipped ...] </albums>

310

C H A P T E R 8 B A C K - E N D I N T E R A C T I O N W I T H A J A X

You want to retrieve this data via XHR and display it as a table in the page. Figure 8-5 shows the different stages of the request.

Figure 8-5. Retrieving and showing XML data as a table

The main part of the script does not have to change:

simpleXMLxhr.js

simplexhr = {

doxhr : function( container, url ) {

if( !document.getElementById || !document.createTextNode ){ return;

}

simplexhr.outputContainer = document.getElementById( container ); if( !simplexhr.outputContainer ) { return; }

var request; try {

request = new XMLHttpRequest();

}catch( error ) { try {

request = new ActiveXObject( "Microsoft.XMLHTTP" );

}catch ( error ) { return true;

}

}

request.open( 'get', url,true );

C H A P T E R 8 B A C K - E N D I N T E R A C T I O N W I T H A J A X

311

request.onreadystatechange = function() { if(request.readyState == 1) {

simplexhr.outputContainer.innerHTML = 'loading...';

}

if(request.readyState == 4) {

if( request.status && /200|304/.test( request.status ) ) { simplexhr.retrieved( request );

}else {

simplexhr.failed( request );

}

}

}

request.setRequestHeader( 'If-Modified-Since', 'Wed, 05 Apr 2006 00:00:00 GMT' );

request.send( null ); return false;

},

The difference is in the retrieved() method that reads the data via responseXML and writes out a data table using the XML as the source of content. Remove the loading message and use the DOM createElement() and createTextNode() methods to create the main table:

simpleXMLxhr.js (continued)

retrieved : function( requester ) { var data = requester.responseXML;

simplexhr.outputContainer.removeChild( simplexhr.outputContainer.firstChild );

var i, albumId, artist, albumTitle, comment, td, tr, th; var table = document.createElement( 'table' );

var tablehead = document.createElement( 'thead' ); table.appendChild( tablehead );

tr = document.createElement( 'tr' ); th = document.createElement( 'th' );

th.appendChild( document.createTextNode( 'ID' ) ); tr.appendChild( th );

th=document.createElement( 'th' );

th.appendChild( document.createTextNode( 'Artist' ) ); tr.appendChild( th );

th = document.createElement( 'th' );

th.appendChild( document.createTextNode( 'Title' ) ); tr.appendChild( th );

th=document.createElement( 'th' );

th.appendChild( document.createTextNode( 'Comment' ) ); tr.appendChild( th );

tablehead.appendChild( tr );

var tablebody = document.createElement( 'tbody' ); table.appendChild( tablebody );

312

C H A P T E R 8 B A C K - E N D I N T E R A C T I O N W I T H A J A X

Notice that when you create tables on the fly, MSIE will not display them unless you nest the rows and cells in a TBODY element. Firefox won’t mind.

Next, loop over all the album elements of the data that was retrieved.

simpleXMLxhr.js (continued)

var albums = data.getElementsByTagName( 'album' );

for( i = 0 ; i < albums.length; i++ ) {

For each album, you read the contents of the XML nodes by their tag name and retrieve their text content via firstChild.nodeValue.

simpleXMLxhr.js (continued)

tr = document.createElement( 'tr' );

albumId = data.getElementsByTagName( 'id' )[i]. firstChild.nodeValue;

artist = data.getElementsByTagName('artist')[i]. firstChild.nodeValue;

albumTitle = data.getElementsByTagName('title')[i]. firstChild.nodeValue;

comment = data.getElementsByTagName('comment')[i]. firstChild.nodeValue;

You use this information to add the data cells to the table via createElement(), createTextNode(), and appendChild().

simpleXMLxhr.js (continued)

td = document.createElement( 'th' );

td.appendChild( document.createTextNode( albumId ) ); tr.appendChild( td );

td = document.createElement( 'td' );

td.appendChild( document.createTextNode( artist ) ); tr.appendChild( td );

td = document.createElement( 'td' );

td.appendChild( document.createTextNode( albumTitle ) ); tr.appendChild( td );

td = document.createElement( 'td' );

td.appendChild( document.createTextNode( comment ) ); tr.appendChild( td );

tablebody.appendChild( tr );

}

C H A P T E R 8 B A C K - E N D I N T E R A C T I O N W I T H A J A X

313

Add the resulting table as a new child element to the output container, and return false to stop the link from loading the XML as a new document. The failed() method stays the same.

simpleXMLxhr.js (continued)

simplexhr.outputContainer.appendChild( table ); return false;

},

failed : function( requester ) {

alert( 'The XMLHttpRequest failed. Status: ' + requester.status ); return true;

}

}

You can see that by doing the “right thing” in terms of DOM scripting, scripts can get rather convoluted. You could cut down the amount of code by using tool methods to create the table rows, but that would mean even more processing, as the methods would have to be called from within a loop.

If you know the XML structure like you do in this example, it is probably a lot faster and easier to use innerHTML and string methods to convert the data. The demo exampleXHRxmlCheat.html does exactly that. Most of the script stays the same, but the retrieved() method is a lot shorter:

simpleXMLxhrCheat.js (excerpt)

retrieved : function( requester ){ var data = requester.responseText;

simplexhr.outputContainer.removeChild( simplexhr.outputContainer.firstChild);

var headrow = '<tr><th>ID</th><th>Artist</th><th> Title</th><th>Comment</th></tr>';

data = data.replace( /<\?.*\?>/g, '' )

data = data.replace( /<(\/*)id>/g, '<$1th>' )

data = data.replace( /<(\/*)(artist|title|comment)>/g, '<$1td>' ) data = data.replace( /<(\/*)albums>/g, '<$1table>' )

data = data.replace( /<(\/*)album>/g, '<$1tr>' );

data = data.replace( /<table>/g, '<table>' + headrow ); simplexhr.outputContainer.innerHTML = data;

return false;

},

You retrieve the data as responseText, remove the “loading...” message, and then create a header table row as a string and store it in the variable headrow. Since responseText is a string, you can use the String.replace() method to change the XML elements.

Start by removing the XML prologue by deleting anything beginning with <? and ending with ?>.

314

C H A P T E R 8 B A C K - E N D I N T E R A C T I O N W I T H A J A X

Note This example uses regular expressions, which you may not know yet, but which we will talk about in more detail in the next chapter. It suffices to say that regular expressions are delimited with slashes and match a certain pattern of text. If there are parentheses inside the slashes, these strings will be stored in variables starting with $; these can be used in the replacement string to stand in for the substrings that match the pattern. For example, the regular expression pattern /<(\/*)id>/g matches everything that starts with a <, followed by an optional / (which is stored as $1 if it is found), followed by the string id and the closing > character. The second parameter, <$1th>, writes out either <th> or </th>, depending on the original id tag being the opening or closing tag. Rather than use regular expressions, you could perform simple string replacement instead:

data = data.replace('<id>', '<th>'); data = data.replace('</id>', '</th>');

Replace the other elements according to this scheme: every albums element becomes a table, every album a tr, every id a th; artist, title, and comment become a td each. Append the headrow string to <table> and store the end result in the outputContainer element using innerHTML.

Replacing XML with JSON

While XML is the undisputed champion of data transfer formats—it is text based and you can ensure validity and systems being able to talk to each other via DTDs, XML Schemata, or RELAX NG—Ajax fans have become more and more aware that it can be quite a drag to convert XML to JavaScript objects.

Instead of reading an XML file as XML and parsing it via the DOM or reading it as text and using regular expressions, it would be a lot easier and less straining to the system to have the data in a format that JavaScript can use directly. This format is called JSON (http://json.org/) and is basically a dataset in object literal notation. The demo exampleJSONxhr.html uses the XML of the earlier example as JSON:

<albums>

<album>

<id>1</id>

<artist>Depeche Mode</artist> <title>Playing the Angel</title>

<comment>They are back and finally up to speed again</comment> </album>

<album>

<id>2</id>

<artist>Monty Python</artist> <title>The final Rip-Off</title>

<comment>Double CD with all the songs</comment> </album>

<album>

<id>3</id>

C H A P T E R 8 B A C K - E N D I N T E R A C T I O N W I T H A J A X

315

<artist>Ms Kittin</artist> <title>I.com</title>

<comment>Good electronica</comment> </album>

</albums>

Converted to JSON, this is as follows:

albums.json

{

'album':

[

{

'id' : '1',

'artist' : 'Depeche Mode', 'title' : 'Playing the Angel',

'comment' : 'They are back and finally up to speed again'

},

{

'id' : '2',

'artist' : 'Monty Python', 'title' : 'The final Rip-Off',

'comment' : 'Double CD wiid all the songs'

},

{

'id' : '3',

'artist' : 'Ms Kittin', 'title' : 'I.com',

'comment' : 'Good electronica'

}

]

}

The benefit is that the data is already in a format that JavaScript can understand, and all you need to do to convert it to objects to display is to use the eval method on the string:

exampleJSONxhr.js (excerpt)

retrieved : function( requester ) { simplexhr.outputContainer.removeChild( simplexhr.outputContainer.firstChild); var content = '<table><thead>';

content += '<tr><th>ID</th><th>Artist</th>'; content += '<th>Title</th><th>Comment</th>'; content += '</tr></thead><tbody>';

var data = eval( '(' + requester.responseText + ')' );