/**
* The IORequest Utility - Provides response data normalization for XML, JSON,
* JavaScript and cache option.
*
* @module aui-io
* @submodule aui-io-request
*/
var L = A.Lang,
isBoolean = L.isBoolean,
isFunction = L.isFunction,
isString = L.isString,
defaults = A.namespace('config.io'),
getDefault = function(attr) {
return function() {
return defaults[attr];
};
},
ACTIVE = 'active',
ARGUMENTS = 'arguments',
AUTO_LOAD = 'autoLoad',
CACHE = 'cache',
CFG = 'cfg',
COMPLETE = 'complete',
CONTENT_TYPE = 'content-type',
CONTEXT = 'context',
DATA = 'data',
DATA_TYPE = 'dataType',
EMPTY_STRING = '',
END = 'end',
FAILURE = 'failure',
FORM = 'form',
GET = 'get',
HEADERS = 'headers',
IO_REQUEST = 'IORequest',
JSON = 'json',
METHOD = 'method',
RESPONSE_DATA = 'responseData',
START = 'start',
SUCCESS = 'success',
SYNC = 'sync',
TIMEOUT = 'timeout',
TRANSACTION = 'transaction',
URI = 'uri',
XDR = 'xdr',
XML = 'xml',
PARSE_ERROR = 'Parser error: IO dataType is not correctly parsing',
ACCEPTS = {
all: '*/*',
html: 'text/html',
json: 'application/json, text/javascript',
text: 'text/plain',
xml: 'application/xml, text/xml'
};
/**
* A base class for IORequest, providing:
* <ul>
* <li>Response data normalization for XML, JSON, JavaScript</li>
* <li>Cache options</li>
* </ul>
*
* Quick Example:<br/>
*
* <pre><code>A.io.request(uri, config);</code></pre>
*
* Check the list of <a href="IORequest.html#configattributes">Configuration Attributes</a> available for
* IORequest.
*
* @param config {Object} Object literal specifying widget configuration properties.
*
* @class IORequest
* @constructor
* @extends Plugin.Base
* @uses io
*/
var IORequest = A.Component.create({
/**
* Static property provides a string to identify the class.
*
* @property IORequest.NAME
* @type String
* @static
*/
NAME: IO_REQUEST,
/**
* Static property used to define the default attribute
* configuration for the IORequest.
*
* @property IORequest.ATTRS
* @type Object
* @static
*/
ATTRS: {
/**
* If <code>true</code> invoke the
* <a href="IORequest.html#method_start">start</a> method automatically,
* initializing the IO transaction.
*
* @attribute autoLoad
* @default true
* @type boolean
*/
autoLoad: {
value: true,
validator: isBoolean
},
/**
* If <code>false</code> the current timestamp will be appended to the
* url, avoiding the url to be cached.
*
* @attribute cache
* @default true
* @type boolean
*/
cache: {
value: true,
validator: isBoolean
},
/**
* The type of the request (i.e., could be xml, json, javascript, text).
*
* @attribute dataType
* @default null
* @type String
*/
dataType: {
setter: function(v) {
return (v || EMPTY_STRING).toLowerCase();
},
value: null,
validator: isString
},
/**
* This is a normalized attribute for the response data. It's useful
* to retrieve the correct type for the
* <a href="IORequest.html#config_dataType">dataType</a> (i.e., in json
* requests the <code>responseData</code>) is a JSONObject.
*
* @attribute responseData
* @default null
* @type String | JSONObject | XMLDocument
*/
responseData: {
setter: function(v) {
return this._setResponseData(v);
},
value: null
},
/**
* URI to be requested using AJAX.
*
* @attribute uri
* @default null
* @type String
*/
uri: {
setter: function(v) {
return this._parseURL(v);
},
value: null,
validator: isString
},
// User readOnly variables
/**
* Whether the transaction is active or not.
*
* @attribute active
* @default false
* @type boolean
*/
active: {
value: false,
validator: isBoolean
},
/**
* Object containing all the
* <a href="io.html#configattributes">IO Configuration Attributes</a>.
* This Object is passed to the <code>A.io</code> internally.
*
* @attribute cfg
* @default Object containing all the
* <a href="io.html#configattributes">IO Configuration Attributes</a>.
* @readOnly
* @type String
*/
cfg: {
getter: function() {
var instance = this;
// keep the current cfg object always synchronized with the mapped public attributes
// when the user call .start() it always retrieve the last set values for each mapped attr
return {
arguments: instance.get(ARGUMENTS),
context: instance.get(CONTEXT),
data: instance.getFormattedData(),
form: instance.get(FORM),
headers: instance.get(HEADERS),
method: instance.get(METHOD),
on: {
complete: A.bind(instance.fire, instance, COMPLETE),
end: A.bind(instance._end, instance),
failure: A.bind(instance.fire, instance, FAILURE),
start: A.bind(instance.fire, instance, START),
success: A.bind(instance._success, instance)
},
sync: instance.get(SYNC),
timeout: instance.get(TIMEOUT),
xdr: instance.get(XDR)
};
},
readOnly: true
},
/**
* Stores the IO Object of the current transaction.
*
* @attribute transaction
* @default null
* @type Object
*/
transaction: {
value: null
},
// Configuration Object mapping
// To take advantages of the Attribute listeners of A.Base
// See: http://yuilibrary.com/yui/docs/io/
/**
* See <a href="http://yuilibrary.com/yui/docs/io/#the-configuration-object">IO
* Configuration</a>.
*
* @attribute arguments
* @default Value mapped on YUI.AUI.defaults.io.
* @type Object
*/
arguments: {
valueFn: getDefault(ARGUMENTS)
},
/**
* See <a href="http://yuilibrary.com/yui/docs/io/#the-configuration-object">IO
* Configuration</a>.
*
* @attribute context
* @default Value mapped on YUI.AUI.defaults.io.
* @type Object
*/
context: {
valueFn: getDefault(CONTEXT)
},
/**
* See <a href="http://yuilibrary.com/yui/docs/io/#the-configuration-object">IO
* Configuration</a>.
*
* @attribute data
* @default Value mapped on YUI.AUI.defaults.io.
* @type Object
*/
data: {
valueFn: getDefault(DATA)
},
/**
* See <a href="http://yuilibrary.com/yui/docs/io/#the-configuration-object">IO
* Configuration</a>.
*
* @attribute form
* @default Value mapped on YUI.AUI.defaults.io.
* @type Object
*/
form: {
valueFn: getDefault(FORM)
},
/**
* Set the correct ACCEPT header based on the dataType.
*
* @attribute headers
* @default Object
* @type Object
*/
headers: {
getter: function(value) {
var header = [];
var instance = this;
var dataType = instance.get(DATA_TYPE);
if (dataType) {
header.push(
ACCEPTS[dataType]
);
}
// always add *.* to the accept header
header.push(
ACCEPTS.all
);
return A.merge(
value, {
Accept: header.join(', ')
}
);
},
valueFn: getDefault(HEADERS)
},
/**
* See <a href="http://yuilibrary.com/yui/docs/io/#the-configuration-object">IO
* Configuration</a>.
*
* @attribute method
* @default Value mapped on YUI.AUI.defaults.io.
* @type String
*/
method: {
valueFn: getDefault(METHOD)
},
/**
* A selector to be used to query against the response of the
* request. Only works if the response is XML or HTML.
*
* @attribute selector
* @type string
*/
selector: {
value: null
},
/**
* See <a href="http://yuilibrary.com/yui/docs/io/#the-configuration-object">IO
* Configuration</a>.
*
* @attribute sync
* @default Value mapped on YUI.AUI.defaults.io.
* @type boolean
*/
sync: {
valueFn: getDefault(SYNC)
},
/**
* See <a href="http://yuilibrary.com/yui/docs/io/#the-configuration-object">IO
* Configuration</a>.
*
* @attribute timeout
* @default Value mapped on YUI.AUI.defaults.io.
* @type Number
*/
timeout: {
valueFn: getDefault(TIMEOUT)
},
/**
* See <a href="http://yuilibrary.com/yui/docs/io/#the-configuration-object">IO
* Configuration</a>.
*
* @attribute xdr
* @default Value mapped on YUI.AUI.defaults.io.
* @type Object
*/
xdr: {
valueFn: getDefault(XDR)
}
},
EXTENDS: A.Plugin.Base,
prototype: {
/**
* Construction logic executed during IORequest instantiation. Lifecycle.
*
* @method initializer
* @protected
*/
init: function(config) {
var instance = this;
IORequest.superclass.init.apply(this, arguments);
instance._autoStart();
},
/**
* Destructor lifecycle implementation for the IORequest class.
* Purges events attached to the node (and all child nodes).
*
* @method destructor
* @protected
*/
destructor: function() {
var instance = this;
instance.stop();
instance.set(TRANSACTION, null);
},
/**
* Applies the <code>YUI.AUI.defaults.io.dataFormatter</code> if defined and return the formatted data.
*
* @method getFormattedData
* @protected
* @return {String}
*/
getFormattedData: function() {
var instance = this;
var value = instance.get(DATA);
var dataFormatter = defaults.dataFormatter;
if (isFunction(dataFormatter)) {
value = dataFormatter.call(instance, value);
}
return value;
},
/**
* Starts the IO transaction. Used to refresh the content also.
*
* @method start
*/
start: function() {
var instance = this;
instance.destructor();
instance.set(ACTIVE, true);
var ioObj = instance._yuiIOObj;
if (!ioObj) {
ioObj = new A.IO();
instance._yuiIOObj = ioObj;
}
var transaction = ioObj.send(
instance.get(URI),
instance.get(CFG)
);
instance.set(TRANSACTION, transaction);
},
/**
* Stops the IO transaction.
*
* @method stop
*/
stop: function() {
var instance = this;
var transaction = instance.get(TRANSACTION);
if (transaction) {
transaction.abort();
}
},
/**
* Invoke the <code>start</code> method (autoLoad attribute).
*
* @method _autoStart
* @protected
*/
_autoStart: function() {
var instance = this;
if (instance.get(AUTO_LOAD)) {
instance.start();
}
},
/**
* Parse the <a href="IORequest.html#config_uri">uri</a> to add a
* timestamp if <a href="IORequest.html#config_cache">cache</a> is
* <code>true</code>. Also applies the
* <code>YUI.AUI.defaults.io.uriFormatter</code>.
*
* @method _parseURL
* @param {String} url
* @protected
* @return {String}
*/
_parseURL: function(url) {
var instance = this;
var cache = instance.get(CACHE);
var method = instance.get(METHOD);
// reusing logic to add a timestamp on the url from jQuery 1.3.2
if ((cache === false) && (method == GET)) {
var ts = +new Date;
// try replacing _= if it is there
var ret = url.replace(/(\?|&)_=.*?(&|$)/, '$1_=' + ts + '$2');
// if nothing was replaced, add timestamp to the end
url = ret + ((ret == url) ? (url.match(/\?/) ? '&' : '?') + '_=' + ts : '');
}
// formatting the URL with the default uriFormatter after the cache timestamp was added
var uriFormatter = defaults.uriFormatter;
if (isFunction(uriFormatter)) {
url = uriFormatter.apply(instance, [url]);
}
return url;
},
/**
* Internal end callback for the IO transaction.
*
* @method _end
* @param {Number} id ID of the IO transaction.
* @param {Object} args Custom arguments, passed to the event handler. See <a href="http://yuilibrary.com/yui/docs/io/#the-configuration-object">IO
* @protected
*/
_end: function(id, args) {
var instance = this;
instance.set(ACTIVE, false);
instance.set(TRANSACTION, null);
instance.fire(END, id, args);
},
/**
* Internal success callback for the IO transaction.
*
* @method _success
* @param {Number} id ID of the IO transaction.
* @param {Object} obj IO transaction Object.
* @param {Object} args Custom arguments, passed to the event handler. See <a href="http://yuilibrary.com/yui/docs/io/#the-configuration-object">IO
* @protected
*/
_success: function(id, obj, args) {
var instance = this;
// update the responseData attribute with the new data from xhr
instance.set(RESPONSE_DATA, obj);
instance.fire(SUCCESS, id, obj, args);
},
/**
* Setter for <a href="IORequest.html#config_responseData">responseData</a>.
*
* @method _setResponseData
* @protected
* @param {Object} xhr XHR Object.
* @return {Object}
*/
_setResponseData: function(xhr) {
var data = null;
var instance = this;
if (xhr) {
var dataType = instance.get(DATA_TYPE);
var contentType = xhr.getResponseHeader(CONTENT_TYPE) || '';
// if the dataType or the content-type is XML...
if ((dataType == XML) ||
(!dataType && contentType.indexOf(XML) >= 0)) {
// use responseXML
data = xhr.responseXML;
// check if the XML was parsed correctly
if (data.documentElement.tagName == 'parsererror') {
throw PARSE_ERROR;
}
}
else {
// otherwise use the responseText
data = xhr.responseText;
}
// empty string is not a valid JSON, convert it to null
if (data === EMPTY_STRING) {
data = null;
}
// trying to parse to JSON if dataType is a valid json
if (dataType == JSON) {
try {
data = A.JSON.parse(data);
}
catch (e) {
// throw PARSE_ERROR;
}
}
else {
var selector = instance.get('selector');
if (data && selector) {
var tempRoot;
if (data.documentElement) {
tempRoot = A.one(data);
}
else {
tempRoot = A.Node.create(data);
}
data = tempRoot.all(selector);
}
}
}
return data;
}
}
});
A.IORequest = IORequest;
/**
* Alloy IO extension
*
* @class A.io
* @static
*/
/**
* Static method to invoke the <a href="IORequest.html">IORequest</a>. Likewise <a href="io.html#method_io">io</a>.
*
* @method A.io.request
* @for A.io
* @param {String} uri URI to be requested.
* @param {Object} config Configuration Object for the <a href="io.html">IO</a>.
* @return {IORequest}
*/
A.io.request = function(uri, config) {
return new A.IORequest(
A.merge(config, {
uri: uri
})
);
};