This topic is not a brave or new one. It’s simply a matter that the question recurs in cyberspace, and I hope to synopsize some easy insights and not have to repeat the offering in external forums again and again (did I mention “and AGAIN”?).
For those of us who rely on JavaScript to manipulate the contents of web pages, AJAX has become a way of life. There exists a huge (and occasionally vitriolic) debate about whether XML is the preferred content delivery scheme (just type “xml vs. json” into your favorite search engine and be prepared for bombardment). This is NOT the topic of the current presentation. The underlying communication stream, based on the XMLHttpRequest object of JavaScript offers a great deal more flexibility than simply a choice of two competing content formats. In the course of being a good ‘net citizen and supplying solutions for my less geekly brethren on Yahoo!Answers (written under the “nom de guerre” richarduie), I frequently encountered questions regarding the means by which to initiate the addition of new, generic, textual content from the clent-side…AJAX anyone?
AJAX doesn’t presume any particular format of content representation. The “X” (from XMLHTTP) is, more than anything else, an unfortunate holdover from “olden tymes,” i.e., circa 2000 (which is almost forever ago in Web years), thanks to the original ActiveX implementation by the evil empire, Microsoft. We’re now pretty well stuck with the usage, but, as long it doesn’t distract you from the underlying nature of the real processes, no worries. Remember that this is plain, old HTTP request/response handling – the response stream is nothing more than text; it may contain references to more complex content, such as images or other media that the browser will be required to fetch and render, but anything that can be addressed by a URL is fair game.
Now, I grant that there are many useful JavaScript libraries already configured to provide AJAX services, e.g., prototype, Dojo, etc. Goodness knows, I’ve applied them during my days of captive employment. The rub is that these libraries frequently demand a bit more transfer bandwidth than I prefer, if my sole purpose is to provide very simple AJAX services. So, like all performance-minded geeks, I’ve written a lighter-weight object to handle the operations of AJAX-based fetching of just any old content (heck, the fully-commented, “War and Peace” version is less than 8K – stripped, it’s TINY). This places a burden on the page-specific scripting to be aware of the nature of the content and how to manage it. However, that burden may be quite trivial and differs little, if indeed any, from what would be required by the application of any other library’s AJAX-management objects. That’s just “gravy” in this context – the real meat and potatoes is the dynamic inclusion of files that were not originally in the web page on an as-needed basis without overhead for some additional “wrapper” format.
So, first, there’s my simple, light-weight XMLHttpRequest object manager:
// X(ml)H(ttp)R(equest)Wrapper Object
// define minimalist wrapper object to manage XMLHttpRequest object for
// AJAX processing to read URLs as simple, plain text
// author: S8Design by Richard.Harrison[at]s8design[dot]co[dot]uk (a.k.a., richarduie[at]yahoo[dot]com)
function XhrWrapper()
{
// private methods - - - - - - - - - - - - - - - - - - - - - - - - -
// utility method to abbreviate calls to getElementById() to get
// element with id {eid}
function get(eid) { return document.getElementById(eid); }
// create an instance of the XMLHttpRequest object
function createRequestObject() {
// initialize return object in function-global scope - if neither
// approach to creation succeeds, return {request} will be null
var request = null;
// minimal browser sniff - STILL can't trust native implementation
// of XMLHttpRequest object in IE (7 or otherwise) - would prefer
// try/catch on attempt to create native object, but that would
// succeed in IE7+, even though the object would be "lame"
var browser = navigator.appName;
if('Microsoft Internet Explorer' == browser) {
// attempt to get ActiveX versions from latest to oldest
request = new ActiveXObject(
'Msxml2.XMLHTTP.6.0' || 'Msxml2.XMLHTTP.3.0' ||
'Msxml2.XMLHTTP' || 'Microsoft.XMLHTTP'
);
}
// use non-ActiveX, native JS implementation of XMLHttpRequest
// object for compliant browsers, e.g., Firefox, Opera, Safari.
else request = new XMLHttpRequest();
return request;
}
// callback for all changes of ready state
function handleStateChange() {
switch (ajaxRequest.readyState) {
case 0: { // created, uninitialized - open() not yet called
break; // no handling at present
}
case 1: { // open() called - send() method not yet called
break; // no handling at present
}
case 2: { // send() called - responseText and -Body unavailable
break; // no handling at present
}
case 3: { // receiving - responseText and -Body still unavailable
break; // no handling at present
}
case 4: { // completed - all data available
doComplete(ajaxRequest.responseText || null);
break;
}
default: {
throw name + '.handleStateChange() could not identify a valid ready state';
}
}
}
// specific handling for request completed ready state
function doComplete(c) {
// c...String content returned as AJAX response
// intitialize optional, additional arguments to external function named
// in {fnComplete} for String concatenation of arguments if they exist
var args = '';
// if arguments available, construct String to embed in eval() call
if (null != fnCompleteArgs) {
var last = fnCompleteArgs.length;
for (var i = 0; i < last; i++) {
args += ',' + fnCompleteArgs[i];
}
}
try {
// pass response contents and any additional arguments to external
// function named in object variable {fnComplete}
eval(fnComplete + '(c' + args + ')');
}
catch (e) {
// since this method is private, instance name is referenced privately
throw name + '.doComplete() failed to interpret expression:\n' +
fnComplete + '(' + c + args + ')\n' + e.toString();
}
}
// private fields - - - - - - - - - - - - - - - - - - - - - - - - -
// object global XMLHttpRequest instance
var ajaxRequest = null;
// name of callback function (as seen from page scope) for onComplete
// state - can be a reference to a public function of this object
var fnComplete;
// optional argument-list to be included after text contents fetched
// by AJAX processing in call to {fnComplete} - array in argument order
// function named in {fnComplete}
var fnCompleteArgs = null;
var name = 'xhr'; // set default name for instance
// public methods - - - - - - - - - - - - - - - - - - - - - - - - -
// get text from a URL, passing results to page-scope function
// named in {complete}, once response processing is complete
this.doGetText = function(url, complete, completeArgs) {
// url............service provider address - can be a file
// complete.......name of function in page namespace to which to pass
// response of AJAX request
// completeArgs...additional arguments required by function named in
// {complete} - optional
// create request object, if not yet done
if (null == ajaxRequest) ajaxRequest = createRequestObject();
// if still null, throw exception and abandon process
if (null == ajaxRequest) {
throw this.getName() + '.doGetText() failed to create valid request object';
}
// assign name of onComplete handler to function global holder
fnComplete = complete;
// if argument-list passed for call to function named in {complete}
// assign those to holder
if (completeArgs && null != completeArgs) fnCompleteArgs = completeArgs;
// initiate fetch dialog
ajaxRequest.open('get', url);
// force content type to be plain text
ajaxRequest.setRequestHeader('Content-Type', 'text/plain;charset=UTF-8');
// if not IE, overrideMimeType allows further insistence that server
// sends plain text mime type in response
if (ajaxRequest.overrideMimeType)
ajaxRequest.overrideMimeType('text/plain;charset=UTF-8');
// do this after open() to avoid IE initialization problems -
// allows reuse of same XHR object...silly IE bug
ajaxRequest.onreadystatechange = handleStateChange;
ajaxRequest.send(null); // fetch!
return false;
}
// append String (in new element) to an existing element of current document
this.appendToElement = function(c, pId, tag) {
// c.....any String (with or without markup)
// pId...id of parent element in page to which to append content -
// optional with default of body
// tag...tag name of element to contain content to be appended -
// optional with default of div
// if argument is missing or null, offer error message
if (!c || null == c) {
// since this method is public, instance name is referenced publicly
throw this.getName() + '.appendToElement(): missing content to append';
}
// ...otherwise (implicit else), add content to page
// default to body, if no parent element id given
var p = (!pId || null == pId)?document.body:get(pId);
// default to div, if no container tag name given
var tag = (!tag || null == tag)?'DIV':tag;
var el = document.createElement(tag); // create new element of type {tag}
el.innerHTML = c; // insert content into element
p.appendChild(el); // append element to parent
}
// public accessors and mutators for private fields - - - - - - -
// get AJAX request object for this instance
this.getRequest = function() { return ajaxRequest; }
// get String name of function in page namespace to be applied as callback
// for completed ready state of AJAX request
this.getFnComplete = function() { return fnComplete; }
// set String name of function in page namespace to be applied as callback
// for completed ready state of AJAX request
this.setFnComplete = function(f) { fnComplete = f; }
// get arguments for function in page namespace to be applied as callback
// for completed ready state of AJAX request - array in argument order of
// callback function
this.getFnCompleteArgs = function() { return fnCompleteArgs; }
// set arguments for function in page namespace to be applied as callback
// for completed ready state of AJAX request - array in argument order of
// callback function
this.setFnCompleteArgs = function(a) { fnCompleteArgs = a; }
// set instance name of object - default name is "xhr"
this.getName = function() { return name; }
// set instance name of object, if default name "xhr" is not to be used
this.setName = function(n) { name = n; }
// public fields - - - - - - - - - - - - - - - - - - - - - - - - -
// none at present
}
I store this script in the file ajaxXhr.js. A simple example of the code for a test page to acquaint you with some of the concerns is:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html lang="en" xml:lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
<style>
</style>
<script type="text/javascript" src="ajaxXhr.js"></script>
<script type="text/javascript">
// create instance in page global scope, using default name xhr
var xhr = new XhrWrapper();
// testing functions
function getUrl(type) {
type = (!type)?'text':type; // set default, if not given
var url = document.getElementById('url').value;
if ('text' == type) {
xhr.doGetText(url, 'xhr.appendToElement');
}
else if ('js' == type) {
xhr.doGetText(url, 'doJs');
}
}
function doJs(c) {
eval('var qwe = ' + c);
alert(qwe);
}
</script>
</head>
<body>
<form>
URL:
<input type="text" id="url" />
<input type="button" value="get file as text" onclick="getUrl('text')" />
<input type="button" value="get file as JS" onclick="getUrl('js')" />
</form>
</body>
</html>
The testing HTML offers two simple handling approaches; either append the retrieved content directly to the page or interpret it as a JavaScript expression. The additional overhead for using the XhrWrapper is contained in the in-page script block (could be excised and placed in some more general.js file, but this was a throw-away testing page, after all).
For instance, S8 uses this model to retrieve content for dynamically composed select/options controls. That is, in the case that there are several, successive sets of options that rely on the user's elections of earlier values, we apply the XhrWrapper object to request services from PHP scripts. The PHP services compose the HTML for the new control without additional structure ("a la" XML, JSON, etc.) for direct inclusion to the page. User selects an option in one control, and AJAX fetches the next control, with values appropriately filtered in the PHP layer based on the user's immediately previous selection. The resulting, pre-composed HTML (text) control is then added to the page (in this case, used to replace the innerHTML of a target element identified by it id attribute).
So, to sum up, whether you apply the XhrWrapper (and you are welcome to do so), your own approach, or some more heavy-weight library, fetching straight, ungarnished, textual content directly from files or services is trivial to accomplish. Look, Ma, no "X"!
