|
|
/* ****************************** 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,
instance = $module.data('module-' + settings.namespace), element = $module.get(), query = arguments[0], methodInvoked = (typeof query == 'string'), queryArguments = [].slice.call(arguments, 1), invokedResponse, className = settings.className, metadata = settings.metadata, errors = settings.errors, module ;
module = { initialize: function() { var exitConditions = false,
runSettings,
loadingTimer = new Date().getTime(), loadingDelay,
promise, url, urlVariables,
formData = {}, data,
ajaxSettings = {}, xhr,
errors = settings.errors ;
// 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(errors.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(errors.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.errors[status] !== undefined) ? settings.errors[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(errors.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(errors.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(errors.missingAction); } } // override with url if specified
if(settings.url) { url = settings.url; module.debug('Getting url', url); } return url; }, url: function(url, urlData) { var missingTerm = false, 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(errors.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) ; }, /* standard module */ setting: function(name, value) { if(value === undefined) { return settings[name]; } settings[name] = value; }, verbose: function() { if(settings.verbose && settings.debug) { module.verbose = Function.prototype.bind.call(console.info, console, settings.moduleName + ':'); } }, debug: function() { if(settings.debug) { module.debug = Function.prototype.bind.call(console.info, console, settings.moduleName + ':'); } }, error: function() { if(console.log !== undefined) { module.error = Function.prototype.bind.call(console.error, console, settings.moduleName + ':'); } }, invoke: function(query, passedArguments, context) { var maxDepth, found ; passedArguments = passedArguments || queryArguments; context = element || context; if(typeof query == 'string' && instance !== undefined) { query = query.split('.'); maxDepth = query.length - 1; $.each(query, function(depth, value) { if( $.isPlainObject( instance[value] ) && (depth != maxDepth) ) { instance = instance[value]; return true; } else if( instance[value] !== undefined ) { found = instance[value]; return true; } module.error(errors.method); return false; }); } if ( $.isFunction( found ) ) { instance.verbose('Executing invoked function', found); return found.apply(context, passedArguments); } return found || false; } };
if(methodInvoked) { if(instance === undefined) { module.initialize(); } invokedResponse = module.invoke(query); } else { if(instance !== undefined) { module.destroy(); } module.initialize(); } return (invokedResponse) ? 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), element = 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 = { moduleName : 'API Module', namespace : 'api',
debug : true, verbose : true,
api : {},
beforeSend : function(settings) { return settings; }, beforeXHR : function(xhr) {},
success : function(response) {}, complete : function(response) {}, failure : function(errorCode) {}, progress : false,
errors : { 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 );
|