|
|
/* ****************************** API Author: Jack Lukic Notes: First Commit May 08, 2012
These are modules which bind API functionality to the DOM
Requires: nada
Initialization: $('.button') .apiButton({ success: function() {} }) ;
in our example api is automapped to an object literal @ quirky.config.endpoint.api
HTML: <div class="button" action="follow" data-id="5">
URL : quirky.config.endpoint.api.follow Given Value: /follow/{$id}/ Sent Value : /follow/5/
(4 ways to map api endpoint, each will be looked for in succession) url mapping order: first : defined in plugin init as url (arbitrary url) second : defined in plugin init as action (action in obj literal grouping 'api') third : defined in data-url fourth : defined in data-action
beforeSend: this callback can be used to modify request settings before XHR it also can be used to look for for pre-conditions to prevent API call by returning "false"
****************************** */
;(function ( $, window, document, undefined ) {
$.api = $.fn.api = function(parameters) {
var settings = $.extend(true, {}, $.api.settings, parameters),
// if this keyword isn't a jQuery object, create one
context = (typeof this != 'function') ? this : $('<div/>'), // context defines the element used for loading/error state
$context = (settings.stateContext) ? $(settings.stateContext) : $(context), // module is the thing that initiates the api action, can be independent of context
$module = typeof this == 'object' ? $(context) : $context,
element = this, time = new Date().getTime(), performance = [],
moduleSelector = $module.selector || '', moduleNamespace = settings.namespace + '-module',
className = settings.className, metadata = settings.metadata, error = settings.error,
instance = $module.data(moduleNamespace),
query = arguments[0], methodInvoked = (instance !== undefined && typeof query == 'string'), queryArguments = [].slice.call(arguments, 1),
module, invokedResponse ;
module = { initialize: function() { var runSettings,
loadingTimer = new Date().getTime(), loadingDelay,
promise, url,
formData = {}, data,
ajaxSettings = {}, xhr ;
// serialize parent form if requested!
if(settings.serializeForm && $(this).toJSON() !== undefined) { formData = module.get.formData(); module.debug('Adding form data to API Request', formData); $.extend(true, settings.data, formData); }
// let beforeSend change settings object
runSettings = $.proxy(settings.beforeSend, $module)(settings);
// check for exit conditions
if(runSettings !== undefined && !runSettings) { module.error(error.beforeSend); module.reset(); return; }
// get real url from template
url = module.get.url( module.get.templateURL() );
// exit conditions reached from missing url parameters
if( !url ) { module.error(error.missingURL); module.reset(); return; }
// promise handles notification on api request, so loading min. delay can occur for all notifications
promise = $.Deferred() .always(function() { if(settings.stateContext) { $context .removeClass(className.loading) ; } $.proxy(settings.complete, $module)(); }) .done(function(response) { module.debug('API request successful'); // take a stab at finding success state if json
if(settings.dataType == 'json') { if (response.error !== undefined) { $.proxy(settings.failure, $context)(response.error, settings, $module); } else if ($.isArray(response.errors)) { $.proxy(settings.failure, $context)(response.errors[0], settings, $module); } else { $.proxy(settings.success, $context)(response, settings, $module); } } // otherwise
else { $.proxy(settings.success, $context)(response, settings, $module); } }) .fail(function(xhr, status, httpMessage) { var errorMessage = (settings.error[status] !== undefined) ? settings.error[status] : httpMessage, response ; // let em know unless request aborted
if(xhr !== undefined) { // readyState 4 = done, anything less is not really sent
if(xhr.readyState !== undefined && xhr.readyState == 4) {
// if http status code returned and json returned error, look for it
if( xhr.status != 200 && httpMessage !== undefined && httpMessage !== '') { module.error(error.statusMessage + httpMessage); } else { if(status == 'error' && settings.dataType == 'json') { try { response = $.parseJSON(xhr.responseText); if(response && response.error !== undefined) { errorMessage = response.error; } } catch(error) { module.error(error.JSONParse); } } } $context .removeClass(className.loading) .addClass(className.error) ; // show error state only for duration specified in settings
if(settings.errorLength > 0) { setTimeout(function(){ $context .removeClass(className.error) ; }, settings.errorLength); } module.debug('API Request error:', errorMessage); $.proxy(settings.failure, $context)(errorMessage, settings, this); } else { module.debug('Request Aborted (Most likely caused by page change)'); } } }) ;
// look for params in data
$.extend(true, ajaxSettings, settings, { success : function(){}, failure : function(){}, complete : function(){}, type : settings.method || settings.type, data : data, url : url, beforeSend : settings.beforeXHR });
if(settings.stateContext) { $context .addClass(className.loading) ; }
if(settings.progress) { module.verbose('Adding progress events'); $.extend(true, ajaxSettings, { xhr: function() { var xhr = new window.XMLHttpRequest() ; xhr.upload.addEventListener('progress', function(event) { var percentComplete ; if (event.lengthComputable) { percentComplete = Math.round(event.loaded / event.total * 10000) / 100 + '%'; $.proxy(settings.progress, $context)(percentComplete, event); } }, false); xhr.addEventListener('progress', function(event) { var percentComplete ; if (event.lengthComputable) { percentComplete = Math.round(event.loaded / event.total * 10000) / 100 + '%'; $.proxy(settings.progress, $context)(percentComplete, event); } }, false); return xhr; } }); }
module.verbose('Creating AJAX request with settings: ', ajaxSettings); xhr = $.ajax(ajaxSettings) .always(function() { // calculate if loading time was below minimum threshold
loadingDelay = ( settings.loadingLength - (new Date().getTime() - loadingTimer) ); settings.loadingDelay = loadingDelay < 0 ? 0 : loadingDelay ; }) .done(function(response) { var context = this ; setTimeout(function(){ promise.resolveWith(context, [response]); }, settings.loadingDelay); }) .fail(function(xhr, status, httpMessage) { var context = this ; // page triggers abort on navigation, dont show error
if(status != 'abort') { setTimeout(function(){ promise.rejectWith(context, [xhr, status, httpMessage]); }, settings.loadingDelay); } else { $context .removeClass(className.error) .removeClass(className.loading) ; } }) ; if(settings.stateContext) { $module .data(metadata.promise, promise) .data(metadata.xhr, xhr) ; } },
get: { formData: function() { return $module .closest('form') .toJSON() ; }, templateURL: function() { var action = $module.data(settings.metadata.action) || settings.action || false, url ; if(action) { module.debug('Creating url for: ', action); if(settings.api[action] !== undefined) { url = settings.api[action]; } else { module.error(error.missingAction); } } // override with url if specified
if(settings.url) { url = settings.url; module.debug('Getting url', url); } return url; }, url: function(url, urlData) { var urlVariables ; if(url) { urlVariables = url.match(settings.regExpTemplate); urlData = urlData || settings.urlData;
if(urlVariables) { module.debug('Looking for URL variables', urlVariables); $.each(urlVariables, function(index, templateValue){ var term = templateValue.substr( 2, templateValue.length - 3), termValue = ($.isPlainObject(urlData) && urlData[term] !== undefined) ? urlData[term] : ($module.data(term) !== undefined) ? $module.data(term) : urlData[term] ; module.verbose('Looking for variable', term, $module, $module.data(term), urlData[term]); // remove optional value
if(termValue === false) { module.debug('Removing variable from URL', urlVariables); url = url.replace('/' + templateValue, ''); } // undefined condition
else if(termValue === undefined || !termValue) { module.error(error.missingParameter + term); url = false; return false; } else { url = url.replace(templateValue, termValue); } }); } } return url; } },
// reset api request
reset: function() { $module .data(metadata.promise, false) .data(metadata.xhr, false) ; $context .removeClass(className.error) .removeClass(className.loading) ; },
setting: function(name, value) { if(value !== undefined) { if( $.isPlainObject(name) ) { $.extend(true, settings, name); } else { settings[name] = value; } } else { return settings[name]; } }, internal: function(name, value) { if(value !== undefined) { if( $.isPlainObject(name) ) { $.extend(true, module, name); } else { module[name] = value; } } else { return module[name]; } }, debug: function() { if(settings.debug) { if(settings.performance) { module.performance.log(arguments); } else { module.debug = Function.prototype.bind.call(console.info, console, settings.moduleName + ':'); module.debug.apply(console, arguments); } } }, verbose: function() { if(settings.verbose && settings.debug) { if(settings.performance) { module.performance.log(arguments); } else { module.verbose = Function.prototype.bind.call(console.info, console, settings.moduleName + ':'); module.verbose.apply(console, arguments); } } }, error: function() { module.error = Function.prototype.bind.call(console.error, console, settings.moduleName + ':'); module.error.apply(console, arguments); }, performance: { log: function(message) { var currentTime, executionTime, previousTime ; if(settings.performance) { currentTime = new Date().getTime(); previousTime = time || currentTime; executionTime = currentTime - previousTime; time = currentTime; performance.push({ 'Element' : element, 'Name' : message[0], 'Arguments' : [].slice.call(message, 1) || '', 'Execution Time' : executionTime }); } clearTimeout(module.performance.timer); module.performance.timer = setTimeout(module.performance.display, 100); }, display: function() { var title = settings.moduleName + ':', totalTime = 0 ; clearTimeout(module.performance.timer); time = false; $.each(performance, function(index, data) { totalTime += data['Execution Time']; }); title += ' ' + totalTime + 'ms'; if(moduleSelector) { title += ' \'' + moduleSelector + '\''; } if( (console.group !== undefined || console.table !== undefined) && performance.length > 0) { console.groupCollapsed(title); if(console.table) { console.table(performance); } else { $.each(performance, function(index, data) { console.log(data['Name'] + ': ' + data['Execution Time']+'ms'); }); } console.groupEnd(); } performance = []; } }, invoke: function(query, passedArguments, context) { var maxDepth, found, response ; passedArguments = passedArguments || queryArguments; context = element || context; if(typeof query == 'string' && instance !== undefined) { query = query.split(/[\. ]/); maxDepth = query.length - 1; $.each(query, function(depth, value) { var camelCaseValue = (depth != maxDepth) ? value + query[depth + 1].charAt(0).toUpperCase() + query[depth + 1].slice(1) : query ; if( $.isPlainObject( instance[value] ) && (depth != maxDepth) ) { instance = instance[value]; } else if( $.isPlainObject( instance[camelCaseValue] ) && (depth != maxDepth) ) { instance = instance[camelCaseValue]; } else if( instance[value] !== undefined ) { found = instance[value]; return false; } else if( instance[camelCaseValue] !== undefined ) { found = instance[camelCaseValue]; return false; } else { module.error(error.method); return false; } }); } if ( $.isFunction( found ) ) { response = found.apply(context, passedArguments); } else if(found !== undefined) { response = found; } if($.isArray(invokedResponse)) { invokedResponse.push(response); } else if(typeof invokedResponse == 'string') { invokedResponse = [invokedResponse, response]; } else if(response !== undefined) { invokedResponse = response; } return found; } };
if(methodInvoked) { if(instance === undefined) { module.initialize(); } module.invoke(query); } else { if(instance !== undefined) { module.destroy(); } module.initialize(); }
return (invokedResponse !== undefined) ? invokedResponse : this ; };
// handle DOM attachment to API functionality
$.fn.apiButton = function(parameters) { $(this) .each(function(){ var // if only function passed it is success callback
$module = $(this), selector = $(this).selector || '',
settings = ( $.isFunction(parameters) ) ? $.extend(true, {}, $.api.settings, $.fn.apiButton.settings, { stateContext: this, success: parameters }) : $.extend(true, {}, $.api.settings, $.fn.apiButton.settings, { stateContext: this}, parameters), module ; module = { initialize: function() { if(settings.context && selector !== '') { $(settings.context) .on(selector, 'click.' + settings.namespace, module.click) ; } else { $module .on('click.' + settings.namespace, module.click) ; } }, click: function() { if(!settings.filter || $(this).filter(settings.filter).size() === 0) { $.proxy( $.api, this )(settings); } } }; module.initialize(); }) ; return this; };
$.api.settings = { name : 'API', namespace : 'api',
debug : true, verbose : true, performance : true,
api : {},
beforeSend : function(settings) { return settings; }, beforeXHR : function(xhr) {}, success : function(response) {}, complete : function(response) {}, failure : function(errorCode) {}, progress : false,
error : { missingAction : 'API action used but no url was defined', missingURL : 'URL not specified for the API action', missingParameter : 'Missing an essential URL parameter: ',
timeout : 'Your request timed out', error : 'There was an error with your request', parseError : 'There was an error parsing your request', JSONParse : 'JSON could not be parsed during error handling', statusMessage : 'Server gave an error: ', beforeSend : 'The before send function has aborted the request', exitConditions : 'API Request Aborted. Exit conditions met' },
className: { loading : 'loading', error : 'error' },
metadata: { action : 'action', promise : 'promise', xhr : 'xhr' },
regExpTemplate: /\{\$([A-z]+)\}/g,
action : false, url : false, urlData : false, serializeForm : false,
stateContext : false,
method : 'get', data : {}, dataType : 'json', cache : true,
loadingLength : 200, errorLength : 2000
};
$.fn.apiButton.settings = { filter : '.disabled, .loading', context : false, stateContext : false };
})( jQuery, window , document );
|