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:
<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:
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:
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 (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);
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.
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:
<div ng-controller="BaseController">
<div class="loading" ng-show="isLoading">
<img src="/Content/images/LOADING .gif" />
</div>
@*REST OF THE APP HTML*@
</div>
CSS:
.loading {
position: fixed;
z-index: 999999;
height: 2em;
width: 2em;
overflow: show;
margin: auto;
top: 0;
left: 0;
bottom: 0;
right: 0;
}
.loading:before {
content: '';
display: block;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.3);
}
.loading:not(:required) {
font: 0/0 a;
color: transparent;
text-shadow: none;
background-color: transparent;
border: 0;
}
@-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.