Let's Make a Framework: Ajax Improvements

2011-09-08 15:00

Let's Make a Framework: Ajax Improvements

by

at 2011-09-08 07:00:00

original http://feedproxy.google.com/~r/dailyjs/~3/WDr1bdF2WBM/framework-79

Let’s Make a Framework is an ongoing series about building a JavaScript framework from the ground up.

These articles are tagged with lmaf. The project we’re creating is called Turing. Documentation is available at turingjs.com.

I was impressed by the Superagent HTTP library, so I decided to see what improvements I could make to turing.net based on it. Along the way I found some IE compatibility issues that I hadn’t spotted before, and improved the functional testing script.

Response and Requests

Both Express and Superagent pass abstracted response and request objects back to callbacks. I found the need for this arose when I noticed IE6 didn’t like me adding properties to the ActiveXObject it uses to support XMLHttpRequest. To fix this I decided to take some inspiration from these projects and provide an abstracted response object to callbacks:

var response = {};

response.status = request.status;
response.responseText = request.responseText;
if (/json/.test(contentType)) {
  response.responseJSON = net.parseJSON(request.responseText);
} else if (/xml/.test(contentType)) {
  response.responseXML = net.parseXML(request.responseText);
}

if (successfulRequest(request)) {
  if (options.success) options.success(response, request);
  if (promise) promise.resolve(response, request);
} else {
  if (options.error) options.error(response, request);
  if (promise) promise.reject(response, request);
}

Now the callbacks get a more friendly response object, as well as the original XMLHttpRequest request object. The JSON and XML tests were also added here — previously only JSON was parsed, but I added support for XML as well.

Parsing XML

In Parsing and serializing XML on MDN, the following fragment is suggested for parsing XML:

var theString='<a id="a"><b id="b">hey!</b></a>';
var parser = new DOMParser();
var dom = parser.parseFromString(theString, "text/xml");
// print the name of the root element or error message
dump(dom.documentElement.nodeName == "parsererror" ? "error while parsing" : dom.documentElement.nodeName);

The resulting XML isn’t parsed into a plain JavaScript Object but a Document instead, which means methods and properties like nodeName are available.

Microsoft’s approach is again to use ActiveXObject:

// Instantiate a DOM object at run time.
var dom = new ActiveXObject("msxml2.DOMDocument.6.0");
dom.async = false;
dom.resolveExternals = false;
dom.loadXML("<a>A</a>");

This is from InstantiateDOM.js at MSDN.

I decided to define a parseXML method once based on browser support:

  /**
    * Parses XML represented as a string.
    *
    * @param {String} string The original string
    * @returns {Object} A JavaScript object
    */
  if (window.DOMParser) {
    net.parseXML = function(text) {
      return new DOMParser().parseFromString(text, 'text/xml');
    };
  } else {
    net.parseXML = function(text) {
      var xml = new ActiveXObject('Microsoft.XMLDOM');
      xml.async = 'false';
      xml.loadXML(text);
      return xml;
    };
  }

To test this, I wrote the following:

'test xml parsing': function() {
  $t.post('/give-me-xml', {
    contentType: 'application/xml',
    success: function(r) {
      assert.equal('key', r.responseXML.documentElement.nodeName);
    }
  });
}

This runs through an Express app, in test/functional/ajax.js.

Promises

As I mentioned, IE doesn’t like modifying the XMLHttpRequest object it provides through ActiveX. I was setting a then property on request objects to support promises which was only used because the XMLHttpRequest was being returned from the network-related methods. To get around the IE issue, I decided just to return an empty object with a then property, so this is still possible:

$t.get('/get-test').then(
  function(r) { assert.equal('Sample text', r.responseText); },
  function(r) { assert.ok(false); }
);

I made the documentation slightly ambiguous about what the network methods return because I suspect returning the current turing object to allow other chaining might be more useful. It currently reads @returns {Object} An object for further chaining with promises, which is what I intended it to do in the first place.

Conclusion

After all that, I never really got to use any of TJ’s ideas from Superagent, other than abstracting response objects. The most important thing to remember when creating cross-browser code is to be careful about extending native objects. That’s a good rule of thumb for JavaScript in general, and I feel doubly embarrassed about doing it in the first place because I’ve been preaching this for a while!

You can get this week’s code in commit 93a5d75.