Recently I was engaged in an AngularJS project that spent a little time switching between views. Okay, well the term “a little” is pretty subjective. As we all know, the more thumb-twiddling a user does while waiting for a response from a server, the less likely they are to use your app, or at the very least – they will use it with growing frustration levels. Even if the server responds within a second or two, a locked up UI always leads to a confusing and frustrating user experience where the user generally becomes click-happy and forces the server to start processing more requests, which leads to longer wait times, which leads to higher wattage use at the server, which robs the user’s microwave of the necessary power to efficiently cook the Hungry Hombré Heat-n-Eat Burrito for lunch, resulting in starving users, angry mobs, broken windows and unemployment … and you wouldn’t want that, would you? WOULD YOU?
But I digress …
I found myself in a somewhat familiar pattern in this project where a route matching one of my when conditions would tee up the view template and controller, after which we were off to the races:
- The controller would call a method on an injected service to getSomeData using some route parameters using the injected route provider.
- The service would place a call to a RESTful API.
- The API would be busy making coffee, shopping for groceries, or whatever APIs do when consuming applications need them most.
- The UI would sit there and look stupid while waiting for the API to get around to processing the request.
- Finally, the API would respond, the promise would be resolved, the $scope would get loaded with the appropriate data model and the UI would render.
- The user had starved to death by this point, so none of it really mattered (kidding .. no users were harmed in the making of this application).
To help keep the angry mobs at bay, I found myself splitting my views up into a content div and a loading-spinner div. Each div used an ng-if and a scope level variable “isLoading” to determine their existence in the DOM. When the controller first fired up, isLoading was immediately set to “true,” showing the loading spinner. As the promise from the service call was resolved successfully, the isLoading variable was set to “false,” hiding the spinner and showing the view content.
After going through this drill in a couple of views, my OCD for code-reuse started hurting my brain. I was also dissatisfied with the tight coupling of the controller to the service and route provider (or even an alternative method I tried by injecting a model that made its own service calls). I determined that I needed to change up my code a bit to honor the following goals:
- I should resolve the model for my controller as the route is processed, thereby injecting a fully resolved model into my controller, relieving my controller of the responsibility of loading data and managing the wait times.
- A loading spinner should be set up as a directive, and placed in my main layout template, making it reusable and available to all views.
- The loading spinner directive should leverage $rootScope to intelligently know when a route is being processed (I know, I know … just remember, the axiom of “Use $rootScope sparingly” != “Never use $rootScope or you WILL DIE”)
The first order of business was to change up my routing so that when the user wanted to view a site’s details, that particular route would set up the view, the controller, and resolve the model dependency as well. The siteModel has a method for getSite that makes a service call over $http to fetch some site details from the server. By the time the controller loads in this instance, the “site” object should be fully resolved.
Routing.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
( function () { "use strict" ; angular.module( "MyApp" ).config( [ "$routeProvider" , "$locationProvider" , function ($routeProvider, $locationProvider) { $locationProvider.html5Mode( true ); $routeProvider .when( "/sites/:id" , { templateUrl: "/app/components/site/siteDetailsTemplate.html" , controller: "siteDetailsController" , resolve: { site: [ "siteModel" , "$route" , function (siteModel, $route) { return siteModel.getSite($route.current.params.id); }] } }); } ] ); })(); |
A quick look into our siteDetailsController shows that the controller is being injected with the resolved “site” and knows nothing about loading the site, or turning on/off a loading spinner, etc.
siteDetailsController.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
( function () { "use strict" ; angular.module( "MyApp.Controllers" ) .controller( "siteDetailsController" , [ "$scope" , "$location" , "site" , function ($scope, $location, site) { $scope.site = site; } ] ); })(); |
Here is the directive for our loading spinner. Note that it uses the $rootScope to listen for route change events. When these events are fired, a variable (“isRouteLoading”) is set to the appropriate boolean value.
waitIndicatorDirective.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
( function () { "use strict" ; angular.module( "MyApp.Directives" ) .directive( "myappWaitIndicator" , [ "$rootScope" , function ($rootScope) { return { restrict: "C" , templateUrl: "/app/directives/waitIndicator/waitIndicatorTemplate.html" , link: function (scope, element, attributes) { scope.isRouteLoading = false ; $rootScope.$on( '$routeChangeStart' , function () { scope.isRouteLoading = true ; }); $rootScope.$on( '$routeChangeSuccess' , function () { scope.isRouteLoading = false ; }); } } } ] ); })(); |
Our wait indicator template: Note the use of the “isRouteLoading” to toggle the presence of this markup in the DOM.
waitIndicatorTemplate.html
1
2
3
|
< div class = "processing-overlay" ng-if = "isRouteLoading" > < div class = "spinner" ></ div > </ div > |
I’ve styled the various bits of the wait indicator to provide a fixed overlay and animated gif.
custom.css
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
.processing-overlay { background-color :rgba( 255 , 255 , 255 , 0.5 ); position : fixed ; z-index : 1000 ; top : 0 ; left : 0 ; width : 100% ; height : 100% ; } .processing-overlay div.spinner { background : url (/Content/img/processing.gif) no-repeat 0 0 ; background-position : center ; height : 100% ; z-index : 1001 ; } |
Here I’ve landed the wait indicator in the main layout.
_MainLayout.cshtml (snippet)
1
2
3
4
5
6
|
< section class = "main-section" > < div class = "myapp-wait-indicator" ></ div > < div ng-view > @RenderBody() </ div > </ section > |
The end result is a nice wait message overlay whenever a route change is in progress!