-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
1 changed file
with
311 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,311 @@ | ||
angular.module('angularPromiseButtons', []); | ||
|
||
angular.module('angularPromiseButtons') | ||
.directive('promiseBtn', ['angularPromiseButtons', '$parse', '$timeout', '$compile', function(angularPromiseButtons, $parse, $timeout, $compile) { | ||
'use strict'; | ||
|
||
return { | ||
restrict: 'EA', | ||
priority: angularPromiseButtons.config.priority, | ||
scope: { | ||
promiseBtn: '=', | ||
promiseBtnOptions: '=?' | ||
}, | ||
link: function(scope, el, attrs) { | ||
// provide configuration | ||
var cfg = angularPromiseButtons.config; | ||
// later initialized via initPromiseWatcher() | ||
var promiseWatcher; | ||
// timeout used | ||
var minDurationTimeout; | ||
// boolean to determine minDurationTimeout state | ||
var minDurationTimeoutDone; | ||
// boolean to determine if promise was resolved | ||
var promiseDone; | ||
|
||
|
||
/** | ||
* Handles everything to be triggered when the button is set | ||
* to loading state. | ||
* @param {Object}btnEl | ||
*/ | ||
function initLoadingState(btnEl) { | ||
if (cfg.btnLoadingClass && !cfg.addClassToCurrentBtnOnly) { | ||
btnEl.addClass(cfg.btnLoadingClass); | ||
} | ||
if (cfg.disableBtn && !cfg.disableCurrentBtnOnly) { | ||
btnEl.attr('disabled', 'disabled'); | ||
} | ||
} | ||
|
||
/** | ||
* Handles everything to be triggered when loading is finished | ||
* @param {Object}btnEl | ||
*/ | ||
function handleLoadingFinished(btnEl) { | ||
if ((!cfg.minDuration || minDurationTimeoutDone) && promiseDone) { | ||
if (cfg.btnLoadingClass) { | ||
btnEl.removeClass(cfg.btnLoadingClass); | ||
} | ||
if (cfg.disableBtn) { | ||
btnEl.removeAttr('disabled'); | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* Initializes a watcher for the promise. Also takes | ||
* cfg.minDuration into account if given. | ||
* @param {Function}watchExpressionForPromise | ||
* @param {Object}btnEl | ||
*/ | ||
function initPromiseWatcher(watchExpressionForPromise, btnEl) { | ||
// watch promise to resolve or fail | ||
scope.$watch(watchExpressionForPromise, function(mVal) { | ||
minDurationTimeoutDone = false; | ||
promiseDone = false; | ||
|
||
// create timeout if option is set | ||
if (cfg.minDuration) { | ||
minDurationTimeout = $timeout(function() { | ||
minDurationTimeoutDone = true; | ||
handleLoadingFinished(btnEl); | ||
}, cfg.minDuration); | ||
} | ||
|
||
// for regular promises | ||
if (mVal && mVal.then) { | ||
initLoadingState(btnEl); | ||
|
||
// angular promise | ||
if (mVal.finally) { | ||
mVal.finally(function() { | ||
promiseDone = true; | ||
handleLoadingFinished(btnEl); | ||
}); | ||
} | ||
// ES6 promises | ||
else { | ||
mVal.then(function() { | ||
promiseDone = true; | ||
handleLoadingFinished(btnEl); | ||
}) | ||
.catch(function() { | ||
promiseDone = true; | ||
handleLoadingFinished(btnEl); | ||
}); | ||
} | ||
|
||
} | ||
// for $resource | ||
else if (mVal && mVal.$promise) { | ||
initLoadingState(btnEl); | ||
mVal.$promise.finally(function() { | ||
promiseDone = true; | ||
handleLoadingFinished(btnEl); | ||
}); | ||
} | ||
}); | ||
} | ||
|
||
|
||
/** | ||
* Get the callbacks from the (String) expression given. | ||
* @param {String}expression | ||
* @returns {Array} | ||
*/ | ||
function getCallbacks(expression) { | ||
return expression | ||
// split by ; to get different functions if any | ||
.split(';') | ||
.map(function(callback) { | ||
// return getter function | ||
return $parse(callback); | ||
}); | ||
} | ||
|
||
/** | ||
* $compile and append the spinner template to the button. | ||
* @param {Object}btnEl | ||
*/ | ||
function appendSpinnerTpl(btnEl) { | ||
btnEl.append($compile(cfg.spinnerTpl)(scope)); | ||
} | ||
|
||
/** | ||
* Used to limit loading state to show only for the currently | ||
* clicked button. | ||
* @param {Object}btnEl | ||
*/ | ||
function addHandlersForCurrentBtnOnly(btnEl) { | ||
// handle current button only options via click | ||
if (cfg.addClassToCurrentBtnOnly) { | ||
btnEl.on(cfg.CLICK_EVENT, function() { | ||
btnEl.addClass(cfg.btnLoadingClass); | ||
}); | ||
} | ||
|
||
if (cfg.disableCurrentBtnOnly) { | ||
btnEl.on(cfg.CLICK_EVENT, function() { | ||
btnEl.attr('disabled', 'disabled'); | ||
}); | ||
} | ||
} | ||
|
||
/** | ||
* Used for the function syntax of the promise button directive by | ||
* parsing the expressions provided by the attribute via getCallbacks(). | ||
* Unbinds the default event handlers, which is why it might sometimes | ||
* be required to use the promise syntax. | ||
* @param {Object}eventToHandle | ||
* @param {String}attrToParse | ||
* @param {Object}btnEl | ||
*/ | ||
function initHandlingOfViewFunctionsReturningAPromise(eventToHandle, attrToParse, btnEl) { | ||
// we need to use evalAsync here, as | ||
// otherwise the click or submit event | ||
// won't be ready to be replaced | ||
scope.$evalAsync(function() { | ||
var callbacks = getCallbacks(attrs[attrToParse]); | ||
|
||
// unbind original click event | ||
el.unbind(eventToHandle); | ||
|
||
// rebind, but this time watching it's return value | ||
el.bind(eventToHandle, function(event) { | ||
// Make sure we run the $digest cycle | ||
scope.$apply(function() { | ||
callbacks.forEach(function(cb) { | ||
// execute function on parent scope | ||
// as we're in an isolate scope here | ||
var promise = cb(scope.$parent, {$event: event}); | ||
|
||
// only init watcher if not done before | ||
if (!promiseWatcher) { | ||
promiseWatcher = initPromiseWatcher(function() { | ||
return promise; | ||
}, btnEl); | ||
} | ||
}); | ||
}); | ||
}); | ||
}); | ||
} | ||
|
||
/** | ||
* Get's all submit button children of the given element | ||
* @param {Object}formEl | ||
* @returns {Object} | ||
*/ | ||
function getSubmitBtnChildren(formEl) { | ||
var submitBtnEls = []; | ||
var allButtonEls = formEl.find(angularPromiseButtons.config.BTN_SELECTOR); | ||
|
||
for (var i = 0; i < allButtonEls.length; i++) { | ||
var btnEl = allButtonEls[i]; | ||
if (angular.element(btnEl) | ||
.attr('type') === 'submit') { | ||
submitBtnEls.push(btnEl); | ||
} | ||
} | ||
return angular.element(submitBtnEls); | ||
} | ||
|
||
|
||
// INIT | ||
// --------- | ||
|
||
// check if there is any value given via attrs.promiseBtn | ||
if (!attrs.promiseBtn) { | ||
// handle ngClick function directly returning a promise | ||
if (attrs.hasOwnProperty(cfg.CLICK_ATTR)) { | ||
appendSpinnerTpl(el); | ||
addHandlersForCurrentBtnOnly(el); | ||
initHandlingOfViewFunctionsReturningAPromise(cfg.CLICK_EVENT, cfg.CLICK_ATTR, el); | ||
} | ||
// handle ngSubmit function directly returning a promise | ||
else if (attrs.hasOwnProperty(cfg.SUBMIT_ATTR)) { | ||
// get child submits for form elements | ||
var btnElements = getSubmitBtnChildren(el); | ||
|
||
appendSpinnerTpl(btnElements); | ||
addHandlersForCurrentBtnOnly(btnElements); | ||
initHandlingOfViewFunctionsReturningAPromise(cfg.SUBMIT_EVENT, cfg.SUBMIT_ATTR, btnElements); | ||
} | ||
} | ||
// handle promises passed via scope.promiseBtn | ||
else { | ||
appendSpinnerTpl(el); | ||
addHandlersForCurrentBtnOnly(el); | ||
// handle promise passed directly via attribute as variable | ||
initPromiseWatcher(function() { | ||
return scope.promiseBtn; | ||
}, el); | ||
} | ||
|
||
|
||
// watch and update options being changed | ||
scope.$watch('promiseBtnOptions', function(newVal) { | ||
if (angular.isObject(newVal)) { | ||
cfg = angular.extend({}, cfg, newVal); | ||
} | ||
}, true); | ||
|
||
// cleanup | ||
scope.$on('$destroy', function() { | ||
$timeout.cancel(minDurationTimeout); | ||
}); | ||
} | ||
}; | ||
}]); | ||
|
||
angular.module('angularPromiseButtons') | ||
.provider('angularPromiseButtons', function angularPromiseButtonsProvider() { | ||
'use strict'; | ||
|
||
// ***************** | ||
// DEFAULTS & CONFIG | ||
// ***************** | ||
|
||
var config = { | ||
spinnerTpl: '<span class="btn-spinner"></span>', | ||
priority: 0, | ||
disableBtn: true, | ||
btnLoadingClass: 'is-loading', | ||
addClassToCurrentBtnOnly: false, | ||
disableCurrentBtnOnly: false, | ||
minDuration: false, | ||
CLICK_EVENT: 'click', | ||
CLICK_ATTR: 'ngClick', | ||
SUBMIT_EVENT: 'submit', | ||
SUBMIT_ATTR: 'ngSubmit', | ||
BTN_SELECTOR: 'button' | ||
}; | ||
|
||
// ***************** | ||
// SERVICE-FUNCTIONS | ||
// ***************** | ||
|
||
|
||
// ************************* | ||
// PROVIDER-CONFIG-FUNCTIONS | ||
// ************************* | ||
|
||
return { | ||
extendConfig: function(newConfig) { | ||
config = angular.extend(config, newConfig); | ||
}, | ||
|
||
|
||
// ************************************************ | ||
// ACTUAL FACTORY FUNCTION - used by the directive | ||
// ************************************************ | ||
|
||
$get: function() { | ||
return { | ||
config: config | ||
}; | ||
} | ||
}; | ||
}); | ||
|