Tuesday, October 11, 2016

Angular Tri-State Loading Directive

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;
  };
});

No comments:

Post a Comment