I had a situation where I wanted to load some data asynchronously and display a visual cue to the user that the action had been completed. However, there was a possibility that more than one result could be returned and the user would have to take some action to confirm which item they want to use. I also wanted to show the user a "loading" message so they'd have the visual cue that something was happening. Since I was already using Angular and Font Awesome I decided the best way to go about it would be to use a directive. I prefer my directives to be configurable as much as possible so I can reuse them in the future with relative ease (that is the point of directives after all). This is what I came up with. I will at some point get it uploaded to Github, but for now this is the code. (You can check it out on
plnkr, too).
First I had to create the directive. I've refactored it a little bit so that it's usable as a separate module by just including 'ea.loading' in your module import list when you declare your module.
Directive Requirements:
- User can configure which icons to use, but defaults will be used if nothing is specified
- User can configure which color(s) to use on which states and no defaults will be used
- User can specify a callback function and parameters for each of the three states separately (for click events only)
- User can specify an initial state, but a default of 0 (do not show) will be used if nothing is specified
Directive Module Code:
angular.module('ea.loading', []).directive('eaTriStateLoading', function() {
return {
restrict: 'E',
scope: {
animate: '@',
initialValue: '@',
loadingCallback: '&',
loadingCallbackParameters: '=',
loadingClass: '@',
loadingColor: '@',
notOkCallback: '&',
notOkClass: '@',
notOkColor: '@',
okCallback: '&',
okClass: '@',
okColor: '@',
state: '='
},
link: {
pre: function(scope, element, attributes) {
scope.animate = scope.animate || true;
scope.state = parseInt(scope.initialValue) || 0;
scope.loadingClass = scope.loadingClass || 'fa-spinner';
scope.okClass = scope.OkClass || 'fa-check';
scope.notOkClass = scope.notOkClass || 'fa-exclamation-triangle';
},
post: function(scope, element, attributes) {
function buildParameterObject(parameters) {
var result = {};
for (var i = parameters.length; --i >= 0;) {
var parameter = parameters[i];
for (var j = Object.keys(parameter).length; --j >= 0;) {
// key is the name of the parameter
// parameter[key] is the name of the property on scope that will contain the value to be passed to the parameter
var key = Object.keys(parameter)[j];
var value = scope.$parent[parameter[key]] || parameter[key].substring(1, parameter[key].length - 1);
result[key] = value;
}
}
return result;
}
scope.click = function($event) {
var parameter = {};
if (scope.state === 1 && !!scope.loadingCallback && angular.isDefined(scope.loadingCallback) && angular.isFunction(scope.loadingCallback)) {
if (!!scope.loadingCallbackParameters) {
parameter = buildParameterObject(scope.loadingCallbackParameters);
}
parameter.$event = $event;
scope.loadingCallback(parameter);
} else if (scope.state === 2 && angular.isDefined(scope.okCallback) && angular.isFunction(scope.okCallback)) {
if (!!scope.okCallbackParameters) {
parameter = buildParameterObject(scope.okCallbackParameters);
}
parameter.$event = $event;
} else if (scope.state === 3 && angular.isDefined(scope.noOkCallback) && angular.isFunction(scope.noOkCallback)) {
if (!!scope.notOkCallbackParameters) {
parameter = buildParameterObject(scope.notOkCallbackParameters);
}
parameter.$event = $event;
scope.notOkCallback(parameter);
}
};
},
},
template: '<i class="fa" ng-class="{\'{{loadingClass}}\': state === 1, \'fa-pulse\': state === 1 && animate === true, \'{{okClass}}\': state === 2, \'{{notOkClass}}\': state === 3}" ng-click="click($event)" ng-style="{color: (state === 1 ? loadingColor : state === 2 ? okColor : notOkColor)}"></i>'
}
});
And some use cases:
<ea-tri-state-loading loading-callback-parameters="model.firstDemoLoadingCallbackParameters" loading-callback="clickCallback(property, model.age, $event)" loading-color="blue" not-ok-color="red" ok-callback="decrement()" ok-color="green" state="model.firstDemoValue"></ea-tri-state-loading>
<ea-tri-state-loading loading-class="fa-circle-o-notch" loading-callback-parameters="[{'property': '\'secondDemoValue\''}]" loading-callback="clickCallback(property, $event)" loading-color="purple" not-ok-color="yellow" ok-color="orange" initial-value="2" state="model.secondDemoValue"></ea-tri-state-loading>
<ea-tri-state-loading animate="false" ok-class="fa-rocket" state="model.thirdDemoValue"></ea-tri-state-loading>
<ea-tri-state-loading not-ok-class="fa-remove" state="model.fourthDemoValue"></ea-tri-state-loading>
<ea-tri-state-loading state="model.fifthDemoValue"></ea-tri-state-loading>
And finally the controller that would back those use cases:
angular.module('demo', ['ea.loading']).controller('mainController', function($scope) {
$scope.states = {
first: 1,
second: 2,
third: 3
};
$scope.model = {
firstDemoValue: 1,
secondDemoValue: 1,
thirdDemoValue: 1,
fourthDemoValue: 2,
fifthDemoValue: 3,
age: 12,
firstDemoLoadingCallbackParameters: [
{'property': '"firstDemoValue"'},
{'value': 'age'}
]
};
$scope.clickCallback = function(property, value, $event) {
console.log('property', property);
console.log('value', value);
console.log('$event', $event);
};
$scope.simulateLoading = function(property) {
$scope.model[property] = 1;
}
$scope.simulateOk = function(property) {
$scope.model[property] = 2;
};
$scope.simulateNotOk = function(property) {
$scope.model[property] = 3;
};
});