AngularJS loading directive
The problem
When you have a view in AngularJS with content that is loaded dynamically from the server (by using the $http service, for instance) you will probably end up with a initial blank space in the page, which will be replaced by the content when the answer arrives from the server.
It would be better to display a loading icon as a placeholder for the content which would be replaced by the content when the data finishes being loaded by your controller. Ideally, you do not want to implement this logic in all controllers that load data dynamically, so it would be nice to implement a solution only once and then reuse it in all your views that expect to wait for some content to be loaded.
The solution
I've created a directive called ng-loading which can be placed as an attribute of the div whose content you want to replace with a loading icon while the real content of the div is still being prepared. The value of the ng-loading attribute is an expression that when evaluated to true will replace the loading icon with the original content of the div. Below is the directive's code:
angular.module('yourModule')
.directive('ngLoading', function (Session, $compile) {
var loadingSpinner = <LOADING_ICON_HTML_HERE>;
return {
restrict: 'A',
link: function (scope, element, attrs) {
var originalContent = element.html();
element.html(loadingSpinner);
scope.$watch(attrs.ngLoading, function (val) {
if(val) {
element.html(originalContent);
$compile(element.contents())(scope);
} else {
element.html(loadingSpinner);
}
});
}
};
});
When this directive is initialized it stores the original content of the div and then replace it with the html of a loading icon. You can use any icon you want here, but I recommend using one from Thobias Ahlin's Spin Kit (http://tobiasahlin.com/spinkit/) because they look awesome. Just make sure to include the related CSS in your stylesheet.
Below is an example of how to use the ng-loading directive in your view:
<div id="customerPanel" ng-loading="customer">
Customer's name: {{customer.name}}
</div>
The content of the customerPanel div will be replaced by a loading icon until the scope's customer variable evaluates to something other than 0, null or undefined (you may change the "if(val)" in the directive's code to "if(typeof val !== 'undefined')" if that bothers you).
We can improve the directive further by providing another attribute containing a message to be displayed when the content that was loading was not found:
link: function (scope, element, attrs) {
var originalContent = element.html();
element.html(loadingSpinner);
scope.$watch(attrs.ngLoading, function (val) {
if(val) {
if(val.notFound) {
element.html(attrs.ngLoadingNotFound ? attrs.ngLoadingNotFound : "Not found.");
} else {
element.html(originalContent);
$compile(element.contents())(scope);
}
} else {
element.html(loadingSpinner);
}
});
}
And the code in the view becomes:
<div id="customerPanel" ng-loading="customer" ng-loading-not-found="Customer not found!">
Customer's name: {{customer.name}}
</div>
In this way, if the scope variable in the ng-loading attribute evaluates to an object containing a property named 'notFound' with the value true, the content of the div will be replaced by the message defined in the attribute 'ng-loading-not-found', or by a default message in case this attribute has not been defined.
If you want, you may also add additional customized CSS classes to the divs where the ng-loading attribute is applied in order to create different sizes for the loading icon. In my project I've created the small, medium and large classes for one of the spinners I took from the aforementioned SpinKit.