Sharing Top Content from the Angular-sphere.

How to do loading spinners, the Angular way.

  • /* * The spinner-service is used by the spinner directive to register new spinners.
  • Our spinnerService allows us to inject it anywhere we need it and invoke a single spinner, a group of spinners, or all spinners.
  • Our spinners have unique names so we need a way to show a spinner by its name.
  • group === group) { delete spinners[name]; } } }, _unregisterAll: function () { for (var name in spinners) { delete spinners[name]; } }, show: function (name) { var spinner = spinners[name]; if (!spinner) { throw new Error(“No spinner named ‘” + name + “‘ is registered.”
  • var api = { name: $scope.name, group: $scope.group, show: function () { $scope.show = true; }, hide: function () { $scope.show = false; }, toggle: function () { $scope.show = !$scope.show; } }; // Register this spinner with the spinner service.

Pro tip: A lot of the code here are examples of how to do things that aren’t actually in the final spinner package, such as building an HTML 5 animated spinner or going through code step by step with lots of comments for learning. If you skim the post and think “that’s too much code” like several commenters seem so intent on saying then by all means do it however you think is best. Nobody is forcing you to do anything. Take it from me though, I’ve witnessed the twist my colleagues get themselves into when trying to toggle a simple spinner half way around their app, traversing scope trees and broadcasting events down the entire scope chain, injecting $rootScope everywhere, etc. Before this all I’ve seen are ugly hacks that require even more code.

@BangBitTech: How to do loading spinners, the Angular way #angularjs #javascript #FrontEnd #html

One interesting problem I had to solve recently was how to elegantly deal with loading spinners on the page without violating separation of concerns. Many times, the spinner element is in an area of the DOM controlled by the relevant controller and you can simply toggle a variable in scope and it just works. For example:

(which is just a wrapper around JQuery or JQlite). If you stoop to that level then you’re coupling the DOM with your controller logic and you make it extremely difficult to test in isolation.

Even if you manage to solve this scenario, how then do you solve situations where you need to show/hide multiple loading spinners? It’s often smart to hide every loading spinner on the page in some sort of global error handler, ensuring that any uncaught exceptions don’t end up leaving an endless spinner behind to frustrate the user. If we want to do this while playing nice with Angular and keeping our app testable, then we need to come up with a generic solution that can be implemented anywhere.

. That idea lasted all of ten seconds in my brain before I realized just how unwieldy that solution was going to be. An evented system sounds like the magic bullet we need, but what happens when in a large single page app, two people create spinners with the same name? Well, two event listeners for the same name are going to be registered. When logic triggers an event that is supposed to show a single spinner, it’s going to show both spinners with duplicate names.

What we really need here is the ability to register spinners with a service and allow that service to keep track of them all in an intuitive way. Rather than subscribe to six different events in every instance of our spinner directive, each directive will simply register itself with a service. Here is an example of a directive I recently put together to do just that.

angular.module(‘angularSpinners’) .directive(‘spinner’, function () { return { restrict: ‘EA’, replace: true, transclude: true, scope: { name: ‘@?’, group: ‘@?’, show: ‘=?’, imgSrc: ‘@?’, register: ‘@?’, onLoaded: ‘&?’, onShow: ‘&?’, onHide: ‘&?’ }, template: [ ‘‘, ‘ ‘, ‘ ‘, ‘‘ ].join(”), controller: function ($scope, spinnerService) { // register should be true by default if not specified. if (!$scope.hasOwnProperty(‘register’)) { $scope.register = true; } else { $scope.register = !!$scope.register; } // Declare a mini-API to hand off to our service so the // service doesn’t have a direct reference to this // directive’s scope. var api = { name: $scope.name, group: $scope.group, show: function () { $scope.show = true; }, hide: function () { $scope.show = false; }, toggle: function () { $scope.show = !$scope.show; } }; // Register this spinner with the spinner service. if ($scope.register === true) { spinnerService._register(api); } // If an onShow or onHide expression was provided, // register a watcher that will fire the relevant // expression when show’s value changes. if ($scope.onShow || $scope.onHide) { $scope.$watch(‘show’, function (show) { if (show && $scope.onShow) { $scope.onShow({ spinnerService: spinnerService, spinnerApi: api }); } else if (!show && $scope.onHide) { $scope.onHide({ spinnerService: spinnerService, spinnerApi: api }); } }); } // This spinner is good to go. // Fire the onLoaded expression if provided. if ($scope.onLoaded) { $scope.onLoaded({ spinnerService: spinnerService, spinnerApi: api }); } } }; });

method accepts an API object for the spinner and files it away for later interaction. Each spinner that registers with the spinner service has a unique name, making it easy to keep track of all our spinners.

The directive would look something like this:

It can be shown by default like this:

, but it also allows you to pass a variable from the parent scope so that you can hide/show the spinner based on your application’s state.

You can give it a group name like this:

You can give your spinner element a loading graphic:

You can also stop the spinner from registering with the spinner service if you need to:

option.

for convenience, but that can also be injected like any other Angular service.

I’m not done showing off our directive quite yet! Did you happen to notice this little tidbit in our directive template just beneath the image element?

directive specifies where transcluded content should be placed. Through the use of transclusion it is possible to supply custom HTML to our spinner instead of or in addition to, our loading graphic. For example, we could make an HTML 5 animation and use it as our spinner instead of an animated gif:

Thanks to Tobias Ahlin for the fancy loading animation. With this we now have a loading spinner that is made entirely out of CSS and placed inside a directive that will manage it for us. Here is a working demo of the fancy spinner above. Supply a dummy email and password and click “Login” to see the spinner in action.

expression will run whenever the spinner is hidden.

displaying a canvas animation is easy:

The next piece of this puzzle is the service logic.

Our service needs to store all of our spinner directive API objects and expose some kind of service API for interacting with them all. Our spinners have unique names so we need a way to show a spinner by its name. We also have the option to specify a group on each spinner so we’ll need a way to show all spinners registered with a specific group. Lastly, it would be nice if there were a way to show/hide all spinners regardless of group.

/* * The spinner-service is used by the spinner directive to register new spinners. * It’s also used by anyone who wishes to interface with the API to hide/show spinners on the page. */ app.factory(‘spinnerService’, function () { // create an object to store spinner APIs. var spinners = {}; return { // private method for spinner registration. _register: function (data) { if (!data.hasOwnProperty(‘name’)) { throw new Error(“Spinner must specify a name when registering with the spinner service.”); } if (spinners.hasOwnProperty(data.name)) { throw new Error(“A spinner with the name ‘” + data.name + “‘ has already been registered.”); } spinners[data.name] = data; }, // unused private method for unregistering a directive, // for convenience just in case. _unregister: function (name) { if (spinners.hasOwnProperty(name)) { delete spinners[name]; } }, _unregisterGroup: function (group) { for (var name in spinners) { if (spinners[name].group === group) { delete spinners[name]; } } }, _unregisterAll: function () { for (var name in spinners) { delete spinners[name]; } }, show: function (name) { var spinner = spinners[name]; if (!spinner) { throw new Error(“No spinner named ‘” + name + “‘ is registered.”); } spinner.show(); }, hide: function (name) { var spinner = spinners[name]; if (!spinner) { throw new Error(“No spinner named ‘” + name + “‘ is registered.”); } spinner.hide(); }, showGroup: function (group) { var groupExists = false; for (var name in spinners) { var spinner = spinners[name]; if (spinner.group === group) { spinner.show(); groupExists = true; } } if (!groupExists) { throw new Error(“No spinners found with group ‘” + group + “‘.”) } }, hideGroup: function (group) { var groupExists = false; for (var name in spinners) { var spinner = spinners[name]; if (spinner.group === group) { spinner.hide(); groupExists = true; } } if (!groupExists) { throw new Error(“No spinners found with group ‘” + group + “‘.”) } }, showAll: function () { for (var name in spinners) { spinners[name].show(); } }, hideAll: function () { for (var name in spinners) { spinners[name].hide(); } } }; });

method is really ever used, usually inside of a global error handler to ensure there are no left behind spinners after uncaught exceptions.

. Every spinner will automatically register itself with the service, then you need only inject that same service anywhere you need it.

I took the liberty of turning this solution into a Bower package. You can find it here if you’d like to use it. Here is a working demonstration as well:

How to do loading spinners, the Angular way.

Comments are closed, but trackbacks and pingbacks are open.