P
Peter Michaux
There is quite a bit tentative code now in the Code Worth Recommending
Project's repository. I think that there was some great discussion
spawned by the gathering of this code.
http://cljs.michaux.ca/trac/browser/trunk/src
The last piece of code that I want to add before finalizing
documentation and some sort of ratification is a "Ajax library". Many
of the pieces are already in the library like form serialization.
I have code for Ajax in a branch of the repository
http://cljs.michaux.ca/trac/browser/branches/peterAjax/src/ajax
and am posting the first bit of it here...
Most JavaScript Ajax libraries are a single blob of code and when new
features are added then everyone using the library gets the new
features and extra bloat. I think it is possible to build and Ajax
library as a core function that uses XHR or an image or iframe or some
other way of initiating communication with the server.
The following is the xhrCore function that can set the fundamental
parts of a request (eg, headers, body, url, callback) and make a
request. Wrappers are intended to add API sugar for the xhrCore.
-----------------------------------------------------------
// xhrCore('example.com/handler.php',
// {method:'POST',
// body:'foo=bar&asdf=qwerty',
// onComplete:function(){alert('complete');}});
if (typeof createXMLHttpRequest != 'undefined' &&
(typeof isXhrSufficientForXhrCore == 'undefined' ||
isXhrSufficientForXhrCore) &&
String.prototype.toUpperCase) {
var xhrCore;
(function() {
function empty() {}
xhrCore = function(url, options) {
options = options || {};
var xhr = createXMLHttpRequest(),
method = (options.method || 'GET').toUpperCase(),
callback = options.onComplete,
// does send really need null or can it be undefined?
body = options.body || null,
headers = options.headers || {};
if (callback) {
xhr.onreadystatechange = function() {
if (xhr.readyState == 4) {
callback(xhr);
// for IE memory leak
xhr.onreadystatechange = empty;
}
};
}
if (method == 'GET' && body) {
url = url + '?' + body;
body = null;
}
xhr.open(method, url, true);
if (method == "POST") {
headers['Content-Type'] = headers['Content-Type'] ||
'application/x-www-form-urlencoded';
}
for (p in headers) {
xhr.setRequestHeader(p, headers[p]);
}
xhr.send(body);
return {xhr:xhr};
}
})();
}
-----------------------------------------------------------
Testing if a browser's xhr object is sufficient is done by the
isXhrSufficientForXhrCore function.
-----------------------------------------------------------
// perhaps don't want these checks on an intranet or behind a login
// where the browsers are known to be new.
if (typeof createXMLHttpRequest != 'undefined') {
var isXhrSufficientForXhrCore = (function() {
var xhr = createXMLHttpRequest();
// NN6.1 can create an XMLHttpRequest object but it doesn't
// function and after creation the object's readyState
// property is 'undefined' when it should be '0'.
// The XHR object in NN6.1 doesn't work.
if (xhr.readyState !== 0) {
return false;
}
// IE 5.01sp2 errors when x.setRequestHeader runs.
// The error says "object doesn't support this property"
// This may be because xhr.open() hasn't been called.
// yet. This is the reason for the try-catch. Since we cannot
// determine if the xhr object has setRequestHeader then
// we must put it down the degeradation path.
//
// Opera 8 XHR doesn't have setRequestHeader() and although
// it seems to be able to make a POST request with a body
// without this function This browser is still going to be
// put down the degredation path. Note Opera 8.0.1 does have
// setRequestHeader() and can POST.
try {
if (!xhr.setRequestHeader) {
return false;
}
} catch(e) {
return false;
}
// NN6.2 can't make POST requests because
// can't have arguments to send()
function cannotPost(xhr) {
try {
xhr.send('asdf');
} catch (e) {
// All calls to xhr.send() should error because there wasn't
// a call to xhr.open() however the normal error is something
// about "not initialized" as expected since xhr.open() was
// not called. NN6.2 gives a different error indicating
// xhr.send() cannot take arguments. Note that for NN6.2
// in other human languages the error is still in English.
// TODO check for toString and indexOf?
if (-1 !== e.toString().indexOf('Could not convert' +
// TRICKY that leading space in next string matters.
// using line break only to make postiong to group
possible
' JavaScript argument arg 0 [nsIXMLHttpRequest.send]')) {
return true;
}
}
return false;
}
if (cannotPost(xhr)) {
return false;
}
return true;
})();
}
-----------------------------------------------------------
I'm not completely happy with how all the feature testing is packaged
in the project so far. I'm more interested in getting correct
algorithms right now. However if someone has a bright idea about how
to package necessary feature tests in a library with such small
granularity, I'd be happy to read about it.
With the xhrCore function wrappers can be added for API sugar. Below
is the status.js wrapper which allows the developer to specify
-----------------------------------------------------------
if (Function.prototype.apply &&
String.prototype.match) {
var statusWrapper = function(original) {
return function(url, options) {
options = options || {};
var handlers = {};
for (var p in options) {
if (p.match(/^on(\d+|Success|Failure|Complete)/)) {
handlers[p] = options[p];
// do this delete because don't want inner wrappers
// to need to work on "branches". Waste of time.
delete options[p];
}
}
options.onComplete = function(xhr) {
var h;
// exploit short circuting of ||
if ((h = handlers['on'+xhr.status]) ||
(h = handlers.onSuccess &&
xhr.status>=200 && xhr.status<300) ||
(h = handlers.onFailure &&
(xhr.status<200 || xhr.status>=300)) ||
(h = handlers.onComplete)) {
h.apply(this, arguments);
}
};
return original(url, options);
}
};
}
-----------------------------------------------------------
With the xhrCore and the statusWrapper, a developer can use these
building blocks to easily build an Ajax library with the API he likes.
Here is an example...
-----------------------------------------------------------
// make a custom "sendAjax" function with the API
// and no extra bloat for API features I don't use
//
if (typeof statusWrapper != 'undefined' &&
typeof xhrCore != 'undefined') {
var sendAjax = statusWrapper(xhrCore);
}
// Use the new sendAjax function`
//
function testAjaxRequest() {
if (typeof sendAjax != 'undefined') {
sendAjax('serverFiles/handler.php',
{on200: function(xhr) {
alert('on200');
},
onComplete:function(xhr) {
alert('onComplete');
}
});
}
}
-----------------------------------------------------------
Many other wrappers and even one other core are in the branch of the
repository I linked to at the top of this post. I'm particularly
curious if Randy Webb's system of script insertion could be written as
a different core. I think so. I believe that another core could be
written for file uploads using an iframe (aka old fashioned remote
scripting).
I think this modular approach (some core functions and various
optional wrappers) is exactly what is needed to avoid the hugely
bloated Ajax libraries out there on the web.
I look forward to reading people's thoughts.
Project's repository. I think that there was some great discussion
spawned by the gathering of this code.
http://cljs.michaux.ca/trac/browser/trunk/src
The last piece of code that I want to add before finalizing
documentation and some sort of ratification is a "Ajax library". Many
of the pieces are already in the library like form serialization.
I have code for Ajax in a branch of the repository
http://cljs.michaux.ca/trac/browser/branches/peterAjax/src/ajax
and am posting the first bit of it here...
Most JavaScript Ajax libraries are a single blob of code and when new
features are added then everyone using the library gets the new
features and extra bloat. I think it is possible to build and Ajax
library as a core function that uses XHR or an image or iframe or some
other way of initiating communication with the server.
The following is the xhrCore function that can set the fundamental
parts of a request (eg, headers, body, url, callback) and make a
request. Wrappers are intended to add API sugar for the xhrCore.
-----------------------------------------------------------
// xhrCore('example.com/handler.php',
// {method:'POST',
// body:'foo=bar&asdf=qwerty',
// onComplete:function(){alert('complete');}});
if (typeof createXMLHttpRequest != 'undefined' &&
(typeof isXhrSufficientForXhrCore == 'undefined' ||
isXhrSufficientForXhrCore) &&
String.prototype.toUpperCase) {
var xhrCore;
(function() {
function empty() {}
xhrCore = function(url, options) {
options = options || {};
var xhr = createXMLHttpRequest(),
method = (options.method || 'GET').toUpperCase(),
callback = options.onComplete,
// does send really need null or can it be undefined?
body = options.body || null,
headers = options.headers || {};
if (callback) {
xhr.onreadystatechange = function() {
if (xhr.readyState == 4) {
callback(xhr);
// for IE memory leak
xhr.onreadystatechange = empty;
}
};
}
if (method == 'GET' && body) {
url = url + '?' + body;
body = null;
}
xhr.open(method, url, true);
if (method == "POST") {
headers['Content-Type'] = headers['Content-Type'] ||
'application/x-www-form-urlencoded';
}
for (p in headers) {
xhr.setRequestHeader(p, headers[p]);
}
xhr.send(body);
return {xhr:xhr};
}
})();
}
-----------------------------------------------------------
Testing if a browser's xhr object is sufficient is done by the
isXhrSufficientForXhrCore function.
-----------------------------------------------------------
// perhaps don't want these checks on an intranet or behind a login
// where the browsers are known to be new.
if (typeof createXMLHttpRequest != 'undefined') {
var isXhrSufficientForXhrCore = (function() {
var xhr = createXMLHttpRequest();
// NN6.1 can create an XMLHttpRequest object but it doesn't
// function and after creation the object's readyState
// property is 'undefined' when it should be '0'.
// The XHR object in NN6.1 doesn't work.
if (xhr.readyState !== 0) {
return false;
}
// IE 5.01sp2 errors when x.setRequestHeader runs.
// The error says "object doesn't support this property"
// This may be because xhr.open() hasn't been called.
// yet. This is the reason for the try-catch. Since we cannot
// determine if the xhr object has setRequestHeader then
// we must put it down the degeradation path.
//
// Opera 8 XHR doesn't have setRequestHeader() and although
// it seems to be able to make a POST request with a body
// without this function This browser is still going to be
// put down the degredation path. Note Opera 8.0.1 does have
// setRequestHeader() and can POST.
try {
if (!xhr.setRequestHeader) {
return false;
}
} catch(e) {
return false;
}
// NN6.2 can't make POST requests because
// can't have arguments to send()
function cannotPost(xhr) {
try {
xhr.send('asdf');
} catch (e) {
// All calls to xhr.send() should error because there wasn't
// a call to xhr.open() however the normal error is something
// about "not initialized" as expected since xhr.open() was
// not called. NN6.2 gives a different error indicating
// xhr.send() cannot take arguments. Note that for NN6.2
// in other human languages the error is still in English.
// TODO check for toString and indexOf?
if (-1 !== e.toString().indexOf('Could not convert' +
// TRICKY that leading space in next string matters.
// using line break only to make postiong to group
possible
' JavaScript argument arg 0 [nsIXMLHttpRequest.send]')) {
return true;
}
}
return false;
}
if (cannotPost(xhr)) {
return false;
}
return true;
})();
}
-----------------------------------------------------------
I'm not completely happy with how all the feature testing is packaged
in the project so far. I'm more interested in getting correct
algorithms right now. However if someone has a bright idea about how
to package necessary feature tests in a library with such small
granularity, I'd be happy to read about it.
With the xhrCore function wrappers can be added for API sugar. Below
is the status.js wrapper which allows the developer to specify
-----------------------------------------------------------
if (Function.prototype.apply &&
String.prototype.match) {
var statusWrapper = function(original) {
return function(url, options) {
options = options || {};
var handlers = {};
for (var p in options) {
if (p.match(/^on(\d+|Success|Failure|Complete)/)) {
handlers[p] = options[p];
// do this delete because don't want inner wrappers
// to need to work on "branches". Waste of time.
delete options[p];
}
}
options.onComplete = function(xhr) {
var h;
// exploit short circuting of ||
if ((h = handlers['on'+xhr.status]) ||
(h = handlers.onSuccess &&
xhr.status>=200 && xhr.status<300) ||
(h = handlers.onFailure &&
(xhr.status<200 || xhr.status>=300)) ||
(h = handlers.onComplete)) {
h.apply(this, arguments);
}
};
return original(url, options);
}
};
}
-----------------------------------------------------------
With the xhrCore and the statusWrapper, a developer can use these
building blocks to easily build an Ajax library with the API he likes.
Here is an example...
-----------------------------------------------------------
// make a custom "sendAjax" function with the API
// and no extra bloat for API features I don't use
//
if (typeof statusWrapper != 'undefined' &&
typeof xhrCore != 'undefined') {
var sendAjax = statusWrapper(xhrCore);
}
// Use the new sendAjax function`
//
function testAjaxRequest() {
if (typeof sendAjax != 'undefined') {
sendAjax('serverFiles/handler.php',
{on200: function(xhr) {
alert('on200');
},
onComplete:function(xhr) {
alert('onComplete');
}
});
}
}
-----------------------------------------------------------
Many other wrappers and even one other core are in the branch of the
repository I linked to at the top of this post. I'm particularly
curious if Randy Webb's system of script insertion could be written as
a different core. I think so. I believe that another core could be
written for file uploads using an iframe (aka old fashioned remote
scripting).
I think this modular approach (some core functions and various
optional wrappers) is exactly what is needed to avoid the hugely
bloated Ajax libraries out there on the web.
I look forward to reading people's thoughts.