AjaxObject Part II

This article continues and concludes a previous discussion of the AjaxObject class, built on top of the YUI Connection Utility. We will be discussing the properties and functions attached to the prototype of AjaxObject. We will not discuss AjaxManager (the singleton managing all instances of AjaxObject) today; it will be covered in a future article.

Find the complete JavaScript at AjaxObject.js.

Example 1: AjaxObject - Protected Objects

/**
 * The configuration of this AjaxObject instance.
 * @property _cfg
 * @type {Object}
 * @const
 * @protected
 */
_cfg: {},

/**
 * The timeout id for the request delay timer.
 * @property _delayTimer
 * @type {Object}
 * @const
 * @protected
 */
_delayTimer: null,

/**
 * The timeout id for the request poll timer.
 * @property _pollTimer
 * @type {Object}
 * @const
 * @protected
 */
_pollTimer: null,

/**
 * The last ajax request.
 * @property _lastRequest
 * @type {Object}
 * @const
 * @protected
 */
_lastRequest: null,

First lets look at the protected properties: _cfg, _delayTimer, _pollTimer, _lastRequest. The _cfg object is the instantiated configuration for the AjaxObject, a mix of default values and those pass to the constructor. The _delayTimer stores the YUI timer object for delaying the AJAX request, and is used when requestDelay is configured. The _pollTimer stores the YUI timer object for repeating AJAX requests, and is used when pollTimeout is configured. The _lastRequest stores the last YUI AJAX object returned from YAHOO.util.Connection.asyncRequest.

Example 2: AjaxObject - Get/Set Functions

/**
 * Fetches a configuration value.
 * @method get
 * @return {String} A configuration value.
 * @public
 */
get: function(key) {
	return this._cfg[key];
},

/**
 * Updates a configuration value.
 * @method set
 * @param key {String} Required. The key to update.
 * @param value {String} Required. The value to update to.
 * @public
 */
set: function(key, value) {
	this._cfg[key] = value;
}

Next there are the get(String) and set(String, String) functions. These methods read and write to the protected _cfg object. They are not required as developers can read/write to the _cfg object directly, however I have added them so AjaxObject will be forward compatible with YUI 3.

Example 3: AjaxObject - startRequest Function

/**
 * Initiates the AJAX request.
 * @method startRequest
 * @param conf {Object} Required. Overloading configuration object.
 * @public
 */
startRequest: function(conf) {
	var cfg = {}, str, url, _this = this, fx, _conf = conf || {};

	LANG.augmentObject(cfg, _this._cfg, true);
	LANG.augmentObject(cfg, _conf, true);
	if (! cfg.url) {throw(Your AjaxObject.startRequest is missing a URL.);}
	if (LANG.isFunction(cfg.callback)) {_this.processResults = cfg.callback;}
	url = cfg.url;

	// data is an array, join it (expects "key=value")
	if (LANG.isArray(cfg.data)) {
		str = cfg.data.join(&);
	}
	// data is an object, join it (expects "key:value")
	else if (LANG.isObject(cfg.data)) {
		str = Object.toQueryString(cfg.data);
	}
	// data is a string, just sent it
	else {
		str = (cfg.data ?  + cfg.data : );
	}

	// GET requests need the data appended to the URL
	if (get === cfg.method) {
		if (-1 === url.indexOf(?)) {url += ?;}
		url += str;
	}
	else {
		cfg.data = str;
	}

	// abort any outstanding previous requests when a new one is made
	if (cfg.abortOnDuplicate) {_this.abort(true);}
	
	fx = function() {
		// actually send the request
		_this._lastRequest = CONN.asyncRequest(cfg.method, _cleanInvalidChunks(url), {
			abort: _this._handleFailure,
			argument: cfg,
			cache: cfg.cache,
			failure: _this._handleFailure,
			scope: _this,
			success: _this._handleSuccess,
			timeout: cfg.timeout
		}, cfg.data);

		_this._lastRequest.argument = cfg; // required for global callbacks.
		return _this._lastRequest;
	};

	if (cfg.requestDelay) {
		_this._delayTimer = LANG.later(cfg.requestDelay, _this, fx);
	}
	else {
		return fx();
	}
},

With that out of the way, lets move onto the first complex function, startRequest(Object). The startRequest(Object) function accepts a single, optional configuration object. This object can override any configuration parameters of the instatiated AjaxObject, without modifying _cfg. By using YAHOO.lang.augmentObject to clone the protected_cfg object first, we create a localized configuration and use that for the request instead (this pattern was introduced in the Augmented Configuration Pattern article). The function then checks if the url is defined (this is the only required configuration value) and sets the success callback to _processResults, which will be executed after AjaxObject initially handles the AJAX response. Next, the function determines if data is set and its type, converting everything to a query string (Object.toQueryString is available at Object.js). If data is set and the method is get, then the query string needs to be appended to the url. When the abortOnDuplicate property is true the abort(Boolean) function is called to stop any current requests. Now, if requestDelay is true the actual AJAX request is delayed, otherwise the request is sent and the YUI Connection object is returned. Notice AjaxObject has hijacked all the callbacks from YAHOO.util.Connection.asyncRequest, so that AJAX-related business logic can be executed before delegating to any outside functions.

Example 4: AjaxObject - _handleSuccess Function

/**
 * Abstract method that handles an AJAX success.
 * @method _handleSuccess
 * @param o {Object} Required. The Yahoo AJAX response.
 * @protected
 */
_handleSuccess: function(o) {
	// retrieve response values, test response type, and initialize local variables
	var args = o.argument,
		doc = (o.responseXML), // parenthesis are necessary for FF3
		txt = unknown == o.responseText ?  : o.responseText,
		hdr = (o.getResponseHeader),
		contentType = (hdr && hdr[Content-Type]) ? hdr[Content-Type] : ,
		isJSON = LANG.isValue(txt) && (-1 !== contentType.indexOf(_F.TYPE_JSON)),
		isXML = LANG.isValue(doc) && (-1 !== contentType.indexOf(_F.TYPE_XML) || -1 !== contentType.indexOf(_F.TYPE_XML_APP)),
		response = null,
		error = null,
		code = 0;
	
	// configured content type matches response
	if ((-1 < args.type.indexOf(_F.TYPE_JSON) && isJSON) || ((-1 < args.type.indexOf(_F.TYPE_XML) || -1 < args.type.indexOf(_F.TYPE_XML_APP)) && isXML)) {
		// this is an XML response, retrieve nodes
		if (isXML) {
			response = doc.getElementsByTagName(response)[0];
			error = doc.getElementsByTagName(error)[0];

			// parse special-error XML
			if (error) {
				code = YD.getContentAsString(error.getElementsByTagName(code)[0]);

				if (code) {
					code = parseInt(code, 10);
					desc = YD.getContentAsString(error.getElementsByTagName(description)[0]);
				}
				else {
					desc = YD.getContentAsString(error);
				}
			}
		}
		// this is a JSON response, convert to JSON
		else if (isJSON) {
			response = LANG.JSON.parse(o.responseText);
		}
	}
	// configured content type does not match response
	else {
		isXML = isJSON = false;
		code = ERROR_CODE_INVALID_CONTENT_TYPE;
		error = Response content type ( + contentType + ) does not match configuration ( + args.type + );
	}
	
	// this is an unknown response, assume error
	if (! (response || error)) {
		isXML = isJSON = false;
		error = txt || unknown error;

		// better error message on AJAX failure
		if (-1 < error.indexOf(Page Not Found)) {
			error = args.method +  request failed:  + args.uri;
		}
	}

	o.response = response;

	// response has error
	if (error) {
		var desc = error;
		o.status = code;
		o.ajaxDescription = desc;
		this._handleFailure(o);
	}
	// successful response
	else if (response) {
		o.argument = args.argument;
		if (this.processResults.call(args.scope, o) && args.pollTimeout) {
			this._pollTimer = LANG.later(args.pollTimeout, this, function() {
				this.startRequest(args);
			});
		}
	}
},

When a request succeeds, the _handleSuccess(Object) function is called by the YUI Connection Utility. The _handleSuccess(Object) function first evaluates if the response is XML or JSON, using the appropriate method to parse the response into data. XML is considered valid if the document is wrapped by a response tag and there is not an error tag. If the response is of an unknown type or does not match the configured content type, then it is also considered an error. Successful parsing to a content type, executes the _processResults function, which is actually a pointer to the configured callback function. The object passed into the callback is the same as the YAHOO Connection Utility response object, except the response parameter has been added, which is the parsed XML or JSON AJAX response (ready to be used by the callback function). If the pollTimeout is configured then the value returned by the callback function is evaluated, and startRequest is called again if that value is true. When there was an invalid response type or a specially formatted XML response, then the _handleSuccess(Object) function defers to the _handleFailure(Object) function. The XML format allows for your AjaxObject to manage common server errors, such as parameter parsing, and intelligently notify your end-user.

Example 5: XML Error Format

<error>
	<code>yourErrorCode</code>
	<description>yourErrorDescriptionForEndUser</description>
</error>

The getContentAsString(XMLElement) converts HTML or XML into a string, similar to innerHTML, which does not work on XML nodes in all browsers. The function is available at Dom.js.

Example 6: AjaxObject - _handleFailure Function

/**
 * Abstract method that handles an AJAX failure.
 * @method _handleFailure
 * @param o {Object} Required. The Yahoo AJAX response.
 * @protected
 */
_handleFailure: function(o) {
	if (_isUnloading) {return;}
	var args = o.argument,
		msg;

	switch (o.status) {
		case ERROR_CODE_SESSION_TIMED_OUT: // session timed out code
			window.location.href = _F.LOGOUT_URL + encodeURIComponent(window.location.href);
			return;

		case ERROR_CODE_INVALID_CONTENT_TYPE: msg = o.ajaxDescription; break;
		case ERROR_CODE_INVALID_PARAMETER: msg = o.ajaxDescription; break;
		case ERROR_PAGE_NOT_FOUND:
			_logError(ERROR_PAGE_NOT_FOUND, Object.toQueryString(args));
			msg = Page not found for url= + args.url;
			break;
		case ERROR_ABORTED:
			_logError(Request Aborted: , Object.toQueryString(args));
			msg = Request aborted (or timed out) for url= + args.url;
			break;
		default:
			_logError(o.status || unknown, Object.toQueryString(args));
			msg = An unknown error occurred on our servers. We recommend refreshing the page and before trying again.;
			break;
	}

	if (msg) {
		alert(Your last request failed, because:\n + msg);
	}
	if (LANG.isFunction(args.failure)) {args.failure(o);}
	if (LANG.isFunction(args.rollback)) {args.rollback(o);}
},

When a request fails (not aborts) or the server returns the error XML, then _handleFailure(Object) is called to delegate the response. First, it checks if the page is not unloading, as some browsers immediately terminate all AJAX requests and you probably do not care if an AJAX request fails since the user is leaving the page (if you do, remove the _isUnloading checks). Next the status parameter is evaluated. This value is either the server response (like 404) or a special error code defined by the developer. On Mint.com we use the error code: "1" to indicate that the user has timed out and should be forwarded to the login page; "2" to indicate mismatched content type; "3" to indicate a parameter parsing issue, which is logged and the user messaged; and any other error message is logged and user messaged. The ajaxDescription parameter is provided by any error code using the special XML syntax and defined by the developer. AjaxObject reserves the error code "-1" for aborted requests; usually requests are aborted when the configured timeout value is reached. Lastly, _handleFailure(Object) executes the _failure(Object) and/or _rollback(Object) functions as configured. Use _failure(Object) to handle additional messaging/logging needed if the AJAX request fails and _rollback(Object) to restore any values to their pre-request state.

Example 7: AjaxObject - abort Function

/**
 * Aborts the request.
 * @method abort
 * @param doNotLog {Boolean} Optional. Do not log this abort.
 * @public
 */
abort: function(doNotLog) {
	if (this._lastRequest) {
		_haltTimer(this._delayTimer);
		_haltTimer(this._pollTimer);
		this._lastRequest.doNotLog = doNotLog;
		CONN.abort(this._lastRequest);
	}
},

The last function abort(Boolean) is called by startRequest(Object) or when the AJAX request aborts for some other reason. When force-ably executed by AjaxObject, we do not want _handleFailure(Boolean) to be called so doNotLog is set to true. Otherwise it is false and an aborted AJAX request will execute the failure code. Before delegating to YAHOO.util.Connection.abort(Object) to actually abort the AJAX request, all timers are reset.

Fortunately, even though AjaxObject is difficult to describe, it is very easy to use. Below is an example of how to use AjaxObject and there is also an AjaxObject Test Page where you can try different configuration options.

Example 8: Using AjaxObject

var ajaxObject = new YAHOO.util.AjaxObject({method:post, url:/testUrl.php});
…
ajaxObject.startRequest({data: testParam=testValue&testParam2=testValue2});
…
ajaxObject.startRequest({data: [testParam=testValue],[testParam2=testValue2], method:get});
…
ajaxObject.startRequest({data: {testParam=testValue,testParam2=testValue2}});

Example 8 makes three AJAX requests to the /testUrl.php&rsquot; page. The first and last requests are both post and the middle request is a get. Each request illustrates the three different ways by which data can be set: a search string, an array of "param=value", or an object with param=value pairs.