Skip to content

Commit

Permalink
check in promise buttons
Browse files Browse the repository at this point in the history
  • Loading branch information
mgasner committed Nov 10, 2017
1 parent 1f22450 commit c6b1285
Showing 1 changed file with 311 additions and 0 deletions.
311 changes: 311 additions & 0 deletions www/lib/angular-promise-buttons.js
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
};
}
};
});

0 comments on commit c6b1285

Please sign in to comment.