Wednesday, May 6, 2009

jQuery

I have just completed an Ajax application that uses the jQuery framework. jQuery's promise is to "write less, do more", which I suppose is the objective of most frameworks. Does jQuery deliver though? In a nutshell, yes.


Writing an Ajax application is hard; not because of network paradigms or Javascript, but because each browser brings with it some quirk. Catering for all of these browsers is simply time consuming so, by hard, I really mean time consuming. It is therefore hard to knock out an Ajax application quickly given the shear amount of cross-browser testing that has to be performed.


jQuery does not eliminate cross-browser issues but it certainly minimises them. With my application, the only real short-fall I found with jQuery was its lack of support for dealing with XML documents (not XHTML). This was primarily due to the lack of XML namespace support and I think that this is an area that jQuery should focus on. However it is important to note that the real culprit for the lack of namespace support is Internet Explorer. In essence there is no support for namespaces with IE. I find this extremely difficult to understand... ok, IE 6 with no namespaces is understandable to a degree, but version 7, and worse even version 8 do not support XML namespaces. Microsoft virtually invented SOAP given their backing of it, yet their browsers can't parse SOAP documents accurately (you can ignore the namespaces, but then you ignore the value of namespaces of course).


Here are some other things I found myself having to cater for explicitely...


iframe shims
iframes shims are provided by a jQuery plugin named bgiframe and you can set up jQuery's dialog so that it uses an iframe. Unfortunately IE6 is not the only browser that requires iframes for the purposes of overlaying html objects on OS rendered content (buttons, applets, objects etc.). I therefore found myself having to roll my own. Here's a snippet of what needs to be done in the case of displaying a login dialog when some label is clicked:



var loginDialog = $("#loginDialog").dialog({
autoOpen: false,
close: function() {
loginDialogIframeShim.css("visibility", "hidden");
},
...
});
var loginDialogIframeShim = $(document.createElement("iframe"));
loginDialogIframeShim.attr("frameborder", "0");
loginDialogIframeShim.attr("scrolling", "no");
loginDialogIframeShim.attr("allowtransparency", "false");
loginDialogIframeShim.css("position", "absolute");
loginDialogIframeShim.css("visibility", "hidden");
$("body").append(loginDialogIframeShim);

var login = $("#login").click(function() {
loginDialog.dialog('open');
var loginDialogParent = loginDialog.parent();
var offset = loginDialogParent.offset();
loginDialogIframeShim.css("left", offset.left + "px");
loginDialogIframeShim.css("top", offset.top + "px");
loginDialogIframeShim.css("width", loginDialogParent.outerWidth() + "px");
loginDialogIframeShim.css("height", loginDialogParent.outerHeight() + "px");
loginDialogIframeShim.css("visibility", "visible");
...
The above works well for Windows FF and IE 7 and Mac OS FF and Safari. I haven't tested other browsers.

Datepicker ranges
It would be great to see the Datepicker (which is really great as is) enhanced to support a date range selection.

XmlHttpRequest
While jQuery does a lot to abstract away the peculiarities of browsers with XmlHttpRequest, I did find myself having to encode credentials for Safari. This is because Safari likes to send credentials as part of the URL initially (if that fails then it tries the conventional way using an authorisation header - I do not understand why).


if($.browser.safari) {
requestUsername = encodeURIComponent(requestUsername);
requestPassword = encodeURIComponent(requestPassword);
}

$.ajax({
url: "...",
dataType: "xml",
username: requestUsername,
password: requestPassword,
...


Date/time utilities
In general terms I found these lacking. Quite often with XML one has to convert from ISO8601 to a Javascript date and back. I needed to provide these functions:



function formatISO8601Time(time) {
var timeStr;

var year = time.getUTCFullYear();
timeStr = "" + year;
timeStr += "-";

var month = time.getUTCMonth() + 1;
if (month < 10) {
month = "0" + month;
} else {
month = "" + month;
}
timeStr += month;
timeStr += "-";

var day = time.getUTCDate();
if (day < 10) {
day = "0" + day;
} else {
day = "" + day;
}
timeStr += day;
timeStr += "T";

var hour = time.getUTCHours();
if (hour < 10) {
hour = "0" + hour;
} else {
hour = "" + hour;
}
timeStr += hour;
timeStr += ":";

var minute = time.getUTCMinutes();
if (minute < 10) {
minute = "0" + minute;
} else {
minute = "" + minute;
}
timeStr += minute;
timeStr += ":";

var second = time.getUTCSeconds();
if (second < 10) {
second = "0" + second;
} else {
second = "" + second;
}
timeStr += second;
timeStr += "Z";

return timeStr;
}

function parseISO8601Time(time) {
var date = new Date();

var year = time.substr(0, 4);
var month = time.substr(5, 2);
var day = time.substr(8, 2);
var hour = time.substr(11, 2);
var minute = time.substr(14, 2);
var second = time.substr(17, 2);

date.setUTCFullYear(year);
date.setUTCMonth(month - 1);
date.setUTCDate(day);
date.setUTCHours(hour);
date.setUTCMinutes(minute);
date.setUTCSeconds(second);
date.setUTCMilliseconds(999);

return date;
}

function formatLocalHoursAndMinutes(time) {
var timeStr;

var hour = time.getHours();
if (hour < 10) {
hour = "0" + hour;
} else {
hour = "" + hour;
}
timeStr = hour;

var minute = time.getMinutes();
if (minute < 10) {
minute = "0" + minute;
} else {
minute = "" + minute;
}
timeStr += minute;

timeStr += "h";

return timeStr;
}


XML Namespace utilities


As mentioned before there is very little support namespaces so here's what I had to roll:



function getElementsByTagNameNS(node, namespaceURI, localName) {
var nodeList;

if (node.getElementsByTagNameNS != null) {
nodeList = node.getElementsByTagNameNS(namespaceURI, localName);
} else {
nodeList = node.getElementsByTagName(resolveNS(namespaceURI) + localName);
}

return nodeList;
}

function getAttributeNS(node, namespaceURI, localName) {
var attrVal;

if (node.getAttributeNS != null) {
attrVal = node.getAttributeNS(namespaceURI, localName);
} else {
attrVal = node.getAttribute(resolveNS(namespaceURI) + localName);
}

return attrVal;
}

function isNode(node, namespaceURI, localName) {
var nodeMatched;
if (node.localName != null) {
nodeMatched = (node.namespaceURI == namespaceURI && node.localName == localName);
} else {
nodeMatched = (node.nodeName == (resolveNS(namespaceURI) + localName));
}

return nodeMatched;
}

function resolveNS(namespaceURI) {
var namespacePrefix;
if (namespaceURI == "http://www.yoururigoeshere") {
namespacePrefix = "ns1:";
} else if (namespaceURI == "you are getting the idea now hopefully") {
namespacePrefix = "ns:";
}
return namespacePrefix;
}


Summary
All in all though jQuery saved me a lot of code, particularly around manipulating and traversing my XHTML DOM. The long short of this was that my XHTML page is semantically correct containing absolutely no erroneous divs, style or class declarations i.e. no presentation considerations.

My last Ajax application was a couple of years ago and I must say that using a framework like jQuery has certainly improved programming here. I think we are now seeing some maturity in the browsers and the Javascript framework.

I look forward to continued use of jQuery.

No comments: