/**
* 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];
};
},
ACCEPTS = {
all: '*/*',
html: 'text/html',
json: 'application/json, text/javascript',
text: 'text/plain',
xml: 'application/xml, text/xml'
};
/**
* A base class for IORequest, providing:
*
* - Response data normalization for XML, JSON, JavaScript
* - Cache options
*
* Check the [live demo](http://alloyui.com/examples/io/).
*
* @class A.IORequest
* @extends Plugin.Base
* @param {Object} config Object literal specifying widget configuration
* properties.
* @uses io
* @constructor
* @example
```
YUI().use(
'aui-io-request',
function (Y) {
Y.io.request(
'https://alloyui.com/io/data/content.html',
{
on: {
success: function() {
var data = this.get('responseData');
alert(data);
}
}
}
);
}
);
```
*/
var IORequest = A.Component.create({
/**
* Static property provides a string to identify the class.
*
* @property NAME
* @type String
* @static
*/
NAME: 'IORequest',
/**
* Static property used to define the default attribute
* configuration for the IORequest.
*
* @property ATTRS
* @type Object
* @static
*/
ATTRS: {
/**
* If `true` invoke the [start](A.IORequest.html#method_start) method
* automatically, initializing the IO transaction.
*
* @attribute autoLoad
* @default true
* @type Boolean
*/
autoLoad: {
value: true,
validator: isBoolean
},
/**
* If `false` 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 || '').toLowerCase();
},
value: null,
validator: isString
},
/**
* This is a normalized attribute for the response data. It's useful to
* retrieve the correct type for the
* [dataType](A.IORequest.html#attr_dataType) (i.e., in json requests
* the `responseData`) 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 [IO Configuration Attributes](A.io.html).
* This Object is passed to the `A.io` internally.
*
* @attribute cfg
* @default Object containing all the [IO Configuration
* Attributes](A.io.html).
* @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 [IO
* Configuration](http://yuilibrary.com/yui/docs/io/#the-configuration-object).
*
* @attribute arguments
* @default Value mapped on YUI.AUI.defaults.io.
* @type Object
*/
arguments: {
valueFn: getDefault('arguments')
},
/**
* See [IO
* Configuration](http://yuilibrary.com/yui/docs/io/#the-configuration-object).
*
* @attribute context
* @default Value mapped on YUI.AUI.defaults.io.
* @type Object
*/
context: {
valueFn: getDefault('context')
},
/**
* See [IO
* Configuration](http://yuilibrary.com/yui/docs/io/#the-configuration-object).
*
* @attribute data
* @default Value mapped on YUI.AUI.defaults.io.
* @type Object
*/
data: {
valueFn: getDefault('data')
},
/**
* See [IO
* Configuration](http://yuilibrary.com/yui/docs/io/#the-configuration-object).
*
* @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('dataType');
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 [IO
* Configuration](http://yuilibrary.com/yui/docs/io/#the-configuration-object).
*
* @attribute method
* @default Value mapped on YUI.AUI.defaults.io.
* @type String
*/
method: {
setter: function(val) {
return val.toLowerCase();
},
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
* @default null
* @type String
*/
selector: {
value: null
},
/**
* See [IO
* Configuration](http://yuilibrary.com/yui/docs/io/#the-configuration-object).
*
* @attribute sync
* @default Value mapped on YUI.AUI.defaults.io.
* @type Boolean
*/
sync: {
valueFn: getDefault('sync')
},
/**
* See [IO
* Configuration](http://yuilibrary.com/yui/docs/io/#the-configuration-object).
*
* @attribute timeout
* @default Value mapped on YUI.AUI.defaults.io.
* @type Number
*/
timeout: {
valueFn: getDefault('timeout')
},
/**
* See [IO
* Configuration](http://yuilibrary.com/yui/docs/io/#the-configuration-object).
*
* @attribute xdr
* @default Value mapped on YUI.AUI.defaults.io.
* @type Object
*/
xdr: {
valueFn: getDefault('xdr')
}
},
/**
* Static property used to define which component it extends.
*
* @property EXTENDS
* @type Object
* @static
*/
EXTENDS: A.Plugin.Base,
prototype: {
/**
* Construction logic executed during IORequest instantiation.
* Lifecycle.
*
* @method initializer
* @param config
* @protected
*/
init: function() {
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 `YUI.AUI.defaults.io.dataFormatter` if
* defined and return the formatted data.
*
* @method getFormattedData
* @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 `start` method (autoLoad attribute).
*
* @method _autoStart
* @protected
*/
_autoStart: function() {
var instance = this;
if (instance.get('autoLoad')) {
instance.start();
}
},
/**
* Parse the [uri](A.IORequest.html#attr_uri) to add a
* timestamp if [cache](A.IORequest.html#attr_cache) is
* `true`. Also applies the `YUI.AUI.defaults.io.uriFormatter`.
*
* @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 [IO](http://yuilibrary.com/yui/docs/io/#the-configuration-object).
* @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 [IO](http://yuilibrary.com/yui/docs/io/#the-configuration-object).
* @protected
*/
_success: function(id, obj, args) {
var instance = this;
// update the responseData attribute with the new data from xhr
instance.set('responseData', obj);
instance.fire('success', id, obj, args);
},
/**
* Setter for [responseData](A.IORequest.html#attr_responseData).
*
* @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('dataType');
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 'Parser error: IO dataType is not correctly parsing';
}
}
else {
// otherwise use the responseText
data = xhr.responseText;
}
// empty string is not a valid 'json', convert it to null
if (data === '') {
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 'Parser error: IO dataType is not correctly parsing';
}
}
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 [IORequest](A.IORequest.html).
* Likewise [IO](A.io.html).
*
* @method A.io.request
* @for A.io
* @param {String} uri URI to be requested.
* @param {Object} config Configuration Object for the [IO](A.io.html).
* @return {IORequest}
*/
A.io.request = function(uri, config) {
return new A.IORequest(
A.merge(config, {
uri: uri
})
);
};