2005-11-16 - Issues when developing AJAX Libraries

This article has been reformulated in a more literal way for a Mercurytide white paper. The white-paper may contains non-uptodate informations.


This present articles does not present what AJAX is. You can find many articles on the web to present or how to use the technology. The aim of the articles is to present the different issues and solutions that you can have when you need to develop your own AJAX library or try to understand AJAX scripts. The article introduces different solution about the cross-browser instantiation of the XMLHttpRequest, how to handle responses and how to execute JavaScript sent in the response. It also presents non-obvious points about the object XMLHttpRequest.

If you find any errors or if you have any comments please contact me at thomas@rabaix.net

XMLHttpRequest Object

This object is used to communicate with the server. Microsoft has first introduced this object for their own product: Internet Explorer. Other browsers have since implemented their own XMLHttpRequest objects. There are two implementations from Microsoft, depends on the version of the browser.

So to get the correct XMLHttpRequest object, you can use this function below:

function getNewXmlHttpRequest() {
    var obj = false;
    try {
        obj = new ActiveXObject('Msxml2.XMLHTTP');
    } catch(e) {
        try {
            obj = new ActiveXObject('Microsoft.XMLHTTP');
        } catch(e) {
            obj = new XMLHttpRequest();
        }
    }
    return obj;
}

And to get the object, just do:

var xmlRequest = getNewXmlHttpRequest();

Request Scope

For security reason, the XMLHttpRequest can only access to the server, which serves the JavaScript file. So you cannot request data from other servers on the client side. However you can create a proxy on the server side to request the data to any other servers.

Sending the request

The HTTP specification defined two types of request : POST and GET. The GET request has an empty body in the request, all variables (if any) are located in the URL. However, with the POST request all variables are located in the request body, and you need to specified the content type of the request to 'application/x-www-form-urlencoded'.

       Post

var params = "var1=toto&var2=titi";
var xmlRequest = getNewXmlHttpRequest();
xmlRequest.open('POST','http://rabaix.net/');
xmlRequest.setRequestHeader("Content-type","application/x-www-form-urlencoded");
xmlRequest.send(params);

     Get

var params = "var1=toto&var2=titi";
var xmlRequest = getNewXmlHttpRequest();
xmlRequest.open('GET','http://rabaix.net/?' + params);
xmlRequest.send(false);

Request Encoding

If you want to send special characters, like non-ASCII characters, you need to send them encoded in UTF-8. In this purpose, you can use the encodeURIComponent function.

var params = "money="+ encodeURIComponent('€ euro');
[...] request code [...]

Request Referrer

When a query is sent to the server, the referrer value in the query may be empty. However for some reasons the script, which handles the request, may require this information. The XMLHttpRequest comes with a method to send specific headers. It is not possible to set the referrer value for security reasons, however any other name can be used, like "X-Referrer" (read the warning note).

var xmlRequest = getNewXmlHttpRequest();
xmlRequest.setRequestHeader("X-Referrer",document.location);

On the server side you can access to the referrer with $_SERVER['X_REFERRER'].

Warning: Some browsers allows developers to change the referrer value. The table below summarize how browsers manages the referrer value in an XMLHttpRequest.

  Send referrer Force referrer sent custom
header values
Firefox 1.0.4
no yes yes
Firefox 1.5 RC2
no yes yes
Internet Explorer
yes no yes
Safari 2.0.2
yes no yes
Opera 8
yes no yes

Note: The good spelling of referrer is referrer and not referer!

Response Reply

The XMLHttpRequest object has an event called "onreadystatechange", this event is called everytime the state to the XmlHttpRequest changes. The state value is accessible with the readyState property. There are 5 different values for the readyState value (from xulplanet):
Note: Most of the browsers do not set all these different states. The table below summarizes the different state event calls for an asynchronous request.

  loading loaded interactive complete
Firefox 1.5
yes yes yes yes
Internet Explorer
yes no no
yes
Safari 2.0.2
not tested yet
Opera 8
no no yes yes

The most reliable value is the complete state. Other event call actions should be avoided.

The other important part of the reply is the HTTP status code. This value is accessible with the status property. The standard correct reply status code has to be 200. If you like to know all the status for the HTTP protocols, check this page : http://www.faqs.org/rfcs/rfc2616.

This code show how to set up the event method and how to analyse the reply :

[...]
xmlRequest.onreadystatechange = mycallbackfunction;

function mycallbackfunction() {
    if(xmlRequest.readyState != 4 && xmlRequest.status != 200) return;
  
    // place here your code
}
[...]

responseXML or responseText ?

When you get the reply from the server, you can access to the data with two properties:
"Which properties do I need to used?" you may ask yourself.

responseXML

This property returns the XML version of the reply. This requires that your reply is valid XML, with no errors or invalid characters. This solution is interesting if the web application is based on REST methods.

Let say, you have this reply:

<reply>
    <vcard>
        <firstname>Thomas</firstname>
        <lastname>Rabaix</lastname>
    </vcard>
</reply>

You can access to the information quickly by using DOM functions

var firstnames = reponseXML.getElementsByTagName('firstname');
alert('The firstname is ' + firstnames[0]);

The responseXML is useful with REST applications because you know how the reply is structured.

However you may want to send a part of html page to append into the current web page. Depend on the user’s browser, the method to access to the serialization version of the responseXML may differ.
Gecko browser (Mozilla, Firefox) and Safari have the XMLSerializer object. Internet Explorer does not implement this object, however you can get the XML by using the 'xml' property.

The code below returns the text version of the XML document.

function getTextVersion(XMLnode) {
    var text;
    try {
        //serialization to string DOM Browser
        var serializer = new XMLSerializer();
        text = serializer.serializeToString(XMLnode);
    } catch(e) {
        // serialization of an XML to String (IE only)
        text = XMLnode.xml;
    }
    return text;
}

node.innerHTML = getTextVersion(xmlResponse);

Know limitation: The XMLSerializer works only in Safari if you want to get the serialization of the root object. The code below does not return the html from the reply below on Safari.

var node = xmlRequest.responseXML.firstChild.firstChild);
getTextVersion(node);                      // return nothing with Safari
getTextVerstion(xmlRequest.responseXML);   // return the text version of the reply

responseText

This properties is well adapted if you wish to easily append HTML to the web page.

document.getElementById('element_to_append').innerHTML = responseText;

not quite exact, put explainations here
Simple and efficient, isn’t it? This will work if your document is not valid and has invalid characters.

Know limitation: when you append the responseText with innerHTML method, browsers ignore all JavaScript’s codes.  Some browsers may transform the innerHTML: Internet Explore ignores the JavaScript code and Firefox may have some strange behaviors.

There are some extra #text elements!

If you work with the responseXML reply, you may have some strange behaviors. Not because of the responseXML, but you may forgot how a XML document is structured and how DOM deals with empty #text node.

Let's take an example :

<flats>
  <flat id="18" name="flat 1" />
</flats>

The structure in memory will look like this :

[NodeElement]
  [#Text]
  [NodeElement]
  [#Text]

But most of us are expecting this structure:

[NodeElement]
  [NodeElement]

In order, to clean the reply, you can apply this function. This function will remove all #text node from the reply.

function removeTextNode(n) {
  var rmnode = new Array();
  var v = "";
  var pos = 0;

  // Get all the #text nodes
  for(var i = 0 ; i < n.childNodes.length ; i++) {
    if(n.childNodes[i].nodeType == 3) {
       v = n.childNodes[i].nodeValue.replace(/^\s*|\s*$/g,"");
       if(v.length == 0) rmnode[pos++] = n.childNodes[i];
    }
   
    if(n.childNodes[i].nodeType == 1) removeTextNode(n.childNodes[i]);
  }

  // Remove the text nodes
  for(i = 0 ; i < rmnode.length ; i++) {
    try {n.removeChild(rmnode[i]);}
    catch(e) {}
  }
}

Where is my getElementById function ?

If you try to use the function getElementById on the responseXML object, then you are going to get mad. This function cannot be used ! The reason is simple the XML returned by the server does not contain any information about the default 'id' attribut. With xhtml document the default 'id' attribut is 'id'. Well your reply is not xhtml, so the getElementById cannot work.

You can provide a specific DTD to the reply : true but only if you use Firefox. Others browser will ignore the DTD embeded in the XML reply.

I didn't look further but it will be possible to create a new xml object with the xhtml namespace and populate this object with the data from the server.

JavaScript

As it is explain in the precedent paragraphs, the JavaScript code is not executed. However the web-application may require to execute JavaScript code embedded into the reply. Depends on the property (responseXML or responseText) that you used, the access to the JavaScript code will be different.

The function below launches JavaScript from the responseXML. The function gets all the script elements and gets the JavaScript code with the text properties.

function launchJavascriptFromXML(responseXML) {
    var scripts = responseXML.getElementsByTagName('script');
    var js = '';
    for(var s = 0; s < scripts.length; s++) {
        if(scripts[s].childNodes[0].nodeValue == null) continue;
           js += scripts[s].childNodes[0].nodeValue;
    }
    eval(js);
}

The function below launches JavaScript from the responseText. The function uses regular expressions to get all the JavaScript code from the reply.

function launchJavascript(responseText) {
  // RegExp from prototype.sonio.net
  var ScriptFragment = '(?:<script.*?>)((\n|.)*?)(?:</script>)';
           
  var match    = new RegExp(ScriptFragment, 'img');
  var scripts  = responseText.match(match);

    if(scripts) {
        var js = '';
        for(var s = 0; s < scripts.length; s++) {
            var match = new RegExp(ScriptFragment, 'im');
            js += scripts[s].match(match)[1];
        }
        eval(js);
    }
}

The both functions use the eval function to execute JavaScript codes from the reply. The scope on the JavaScript is global, that means the code can execute external functions already loaded into the page and access to global variables. However if the JavaScript sent by the server contains functions, these functions will not be accessible from any others scripts once the eval function is over.

Note: The JavaScript should be called after any transformations on the page, such as an insert HTML into the page. The reason is that your javascript can access to a DOM element presents in the server reply and if the element does not exist yet that will raise an error.

Interesting point: It is possible to launch automatically javascript embedded into the page. This solution works only with Firefox and Opera. And I think that will suprise some people ;)

function appendAndLaunchJs() {
    if(!isRequest()) return;
   
    var inner = document.getElementById('innerHTML');

    var node = document.createElement('div'); // create a new div element
    node.innerHTML = xmlRequest.responseText; // append the text to node
    inner.innerHTML = '';                     // clear the destination node
    inner.appendChild(node);                  // append the node as a child and ...
                                              // ... at this point the JS is executed
}

AJAX nested into AJAX

If you need to create an XMLHttpRequest inside an XMLHttpRequest function, the reply will be lost because the variable referring to the XMLHttpRequest will be lost. The easy way to resolve this is to create in your library a function to store the XMLHttpRequest object. This function can be call in your embedded JavaScript.

var AJAX_Objects = new Array();
function registerAjaxObject(name, obj) {
    AJAX_Objects[name] = obj;
}

Code in your embedded AJAX reply:

function onreadystatechange_embeded() {
    // get back the object
    if(AJAX_Objects["xmlRequestEmbeded"].readyState == 4) {
        alert('Hello I came from onreadystatechange_embeded function embeded on the xml');
    }
}

var xmlRequestEmbeded = getNewXmlHttpRequest();
xmlRequestEmbeded.open("GET", SERVER + "/data/issues_developing_ajax_libraries/test.xml");
xmlRequestEmbeded.onreadystatechange = onreadystatechange_embeded;
registerAjaxObject("xmlRequestEmbeded", xmlRequestEmbeded);
xmlRequestEmbeded.send(false);

Ressources

Final words

Building an AJAX library is not so easy as a lot of parameters need to be well understood by the developer. The article has presented some problems that you may have: cross-browser instantiation, handling responses and executing JavaScript sent in the response.
There are many different AJAX libraries available on the web to help you building your "Web 2.0" application. This article purpose is to understand these libraries and give first details for whom that wish to put her/his hand into the code.

if you have any comments, please send a email to : thomas.rabaix@gmail.com

Licence of this document

Creative Commons License
This work is licensed under a Creative Commons Attribution 2.0 France License.

Comments

comments powered by Disqus