Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / CSS3

AngularJS - Loader and Error Handling Infrastructure

4.00/5 (6 votes)
24 Feb 2016CPOL2 min read 16.5K  
Demonstrate how to write a simplified AngularJS Loader and Error handling infrastructure

Introduction

This session will demonstrate how to write a simplified AngularJS infrastructure to handle:

  • Loading indicator on UI
  • Simplify data getter (reponse.data, why?)
  • Authentication, Errors and Exception handling

Going Angular

Angular app is usually using some router, but is controlled by a single base controller implicitly on the HTML or by declaring the controller using the router, for the example we will use:

JavaScript
<body ng-controller="BaseController">

This way, we can make general and common handling based on a single point of concern in the entire app.

So for us to start, we need a single point of access to the server, an entity which will handle all in-out communication.

For this purpose, we will create an HttpInterceptor, angular allows us to intercept and inject our logic using:

JavaScript
angular.module('App.Common').factory('httpInterceptor', function ($q, $rootScope) {

    return {
      'request': function (config) {
      },

      'response': function (response) {
      },

      'responseError': function (rejection) {
    };
  });

So the interceptor consists of the following template:

  • Request:
    • All Http Requests will pass here with some "config".
  • Response:
    • All server 200 responses will pass here, this point will signal the app that server processing is done.
  • ResponseError:
    • All server errors will pass here, here we will handle all our error handlings.

Let's add the implementation:

JavaScript
angular.module('App.Common').factory('httpInterceptor', function ($q, $rootScope) {

    var _isLoaderDisplayed = false;
    var _timer = null;
    var _requestCounter = 0;

    return {
      'request': function (config) {
        _requestCounter++;

        if (!_isLoaderDisplayed) {
          if (_timer != null) {
            clearTimeout(_timer);
            _timer = null;
          }

          if (config.dataFetch === undefined || config.dataFetch) {
            _timer = setTimeout(function() {
              _isLoaderDisplayed = true;
              $rootScope.$broadcast('onLoadStart');
            }, 300);
          }
        }

        return config;
      },

      'response': function (response) {
        _requestCounter--;

        if (_requestCounter == 0) {

          if (_timer != null) {
            clearTimeout(_timer);
            _timer = null;
          }

          _isLoaderDisplayed = false;
          $rootScope.$broadcast('onLoadEnd');
        }
        
        // If the response is not data requested by the app service return the response as is
        if (response.config.dataFetch === undefined) {
          return response;
        }

        return response.data;
      },

      'responseError': function (rejection) {
        _requestCounter--;

        if (_timer != null) {
          clearTimeout(_timer);
          _timer = null;
        }

        _isLoaderDisplayed = false;
        $rootScope.$broadcast('onLoadEnd');

        $rootScope.$broadcast('onHttpError', rejection);

        // do something on error (500,401)
        return $q.reject(rejection);
      }
    };
  });

All code is straight forward and you can read and see what's going on in there.

The flow is:

  • Request coming in
  • A timer is being activated so if the request takes more than 300ms, the timeout will fire a
    "onLoadStart" event
  • On response, we are checking if all requests are completed by counting each request on its start when all is done, fire "onLoadEnd"
  • On Response Error, fire both "onLoadEnd" and "onHttpError" and reject the promise.

Note: We've added a special flag: dataFetch. This flag is "Quieting" the onLoadStart event so when needed, the server call is silent. Also note that on the response, we are returning "response.data" to iliminate doing this on each response and usage over and over again.

Next, the angular implementation will be on the "BaseController" which was mentioned at the start, I've added a flag on the $rootScope to indicate if the app is in "Loading" state which we will bind later with the html loader element.

JavaScript
function BaseController($scope, $rootScope, $timeout, PopupFactory) {

    $rootScope.isLoading = false;

    $scope.$on('onLoadStart', function (event, data) {
      $timeout(function() {
        $scope.$apply(function() {
          $rootScope.isLoading = true;
        }, 0, false);

      });
    });

    $scope.$on('onLoadEnd', function (event, data) {
      $timeout(function () {
        $scope.$apply(function () {
          $rootScope.isLoading = false;
        });
      }, 0, false);
    });

    $scope.$on('onHttpError', function (event, data) {

      $timeout(function () {
        $scope.$apply(function () {
          $rootScope.isLoading = false;
        });
      }, 0, false);
      
      switch (data.status) {
        case 500:
          PopupFactory.openError("Unhandled Error", 
          "An unhandled server error has occurred. Please contact R&D.");
          break;
        case 403:
          PopupFactory.openError("Forbidden", 
          "You are trying to access forbidden path. Please contact R&D.");
          break;
        case 401:
          location = "your'e login page url"
          break;        
        default:
          PopupFactory.openError("Unknown Error", 
          "An error has occurred. Please contact R&D.");
          break;
      }
    });
  }

This parts integrates the interceptor with the controller it listens to the events from before and handles each:

  • onLoadStart - set isLoading = true
  • onLoadEnd - set isLoading = false
  • onHttpError - set isLoading = false and handle error code's 500 - error, 403 - forbidden, 401 - redirect to login page and a default one to cover all cases (all displaying an error popup of some sort).

Note: In the following articles, I'll explain a concept of "Business Exceptions" and will expand some of these features.

The final step is the HTML:

JavaScript
<div ng-controller="BaseController">
    <div class="loading" ng-show="isLoading">
        <img src="/Content/images/LOADING .gif" />
    </div>

    @*REST OF THE APP HTML*@
</div>

CSS:

JavaScript
/*LOADING START*/

.loading {
  position: fixed;
  z-index: 999999;
  height: 2em;
  width: 2em;
  overflow: show;
  margin: auto;
  top: 0;
  left: 0;
  bottom: 0;
  right: 0;
}

  /* Transparent Overlay */
  .loading:before {
    content: '';
    display: block;
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background-color: rgba(0,0,0,0.3);
  }

  /* :not(:required) hides these rules from IE9 and below */
  .loading:not(:required) {
    /* hide "loading..." text */
    font: 0/0 a;
    color: transparent;
    text-shadow: none;
    background-color: transparent;
    border: 0;
  }

/* Animation */

@-webkit-keyframes spinner {
  0% {
    -webkit-transform: rotate(0deg);
    -moz-transform: rotate(0deg);
    -ms-transform: rotate(0deg);
    -o-transform: rotate(0deg);
    transform: rotate(0deg);
  }

  100% {
    -webkit-transform: rotate(360deg);
    -moz-transform: rotate(360deg);
    -ms-transform: rotate(360deg);
    -o-transform: rotate(360deg);
    transform: rotate(360deg);
  }
}

@-moz-keyframes spinner {
  0% {
    -webkit-transform: rotate(0deg);
    -moz-transform: rotate(0deg);
    -ms-transform: rotate(0deg);
    -o-transform: rotate(0deg);
    transform: rotate(0deg);
  }

  100% {
    -webkit-transform: rotate(360deg);
    -moz-transform: rotate(360deg);
    -ms-transform: rotate(360deg);
    -o-transform: rotate(360deg);
    transform: rotate(360deg);
  }
}

@-o-keyframes spinner {
  0%; {
    -webkit-transform: rotate(0deg);
    -moz-transform: rotate(0deg);
    -ms-transform: rotate(0deg);
    -o-transform: rotate(0deg);
    transform: rotate(0deg);
  }

  100% {
    -webkit-transform: rotate(360deg);
    -moz-transform: rotate(360deg);
    -ms-transform: rotate(360deg);
    -o-transform: rotate(360deg);
    transform: rotate(360deg);
  }
}

@keyframes spinner {
  0% {
    -webkit-transform: rotate(0deg);
    -moz-transform: rotate(0deg);
    -ms-transform: rotate(0deg);
    -o-transform: rotate(0deg);
    transform: rotate(0deg);
  }

  100% {
    -webkit-transform: rotate(360deg);
    -moz-transform: rotate(360deg);
    -ms-transform: rotate(360deg);
    -o-transform: rotate(360deg);
    transform: rotate(360deg);
  }
}

Summary

In few lines of code, we wrote an infrastructured base implementation that is needed in all web apps, loading and error handling.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)