Tuesday, February 19, 2013

Angularjs & Yeoman - Part 4: Global loading indicator

Introduction

In the previous posts, we've looked into Angularjs services and controllers. This time, we're going to show how to create a Loading indicator (like the "Loading..." box in your Gmail), that can be triggered on and off from within any part of the application. The app does nothing else than load some entries from an API via a $resource, and since that takes time, we want to inform the user that things are being loaded. Code can be pulled from ssh://git@bitbucket.org/mchauvinc/globalloader.git

Bootstrapping

Using yeoman, let's bootstrap this application:
yeoman init angular bubble

Let's also bootstrap a tweets route
yeoman init angular:route genres

And a service that will grab the genres from an API
yeoman init angular:service GenresService

One more item, will be a constant containing the API url, in case we want to globally change that later. This can be done via a service
yeoman init angular:service ApiUrl --value

Services

ApiUrl constant

The angular:service generator, doesn't seem to care for --value and we're getting a factory every time. Let's change /app/scripts/services/ApiUrl.js to:
bubbleApp.value('ApiUrl', "http://epg.suterastudio.com/api/genres");

GenresService

Since GenresService will return a $resource, we need to include ngResource into the app:
  1. yeoman install angular-resource
  2. Add the JS file to your index.html: <script src="components/angular-resource/angular-resource.js"></script>
  3. Add the dependency into your app (app/scripts/app.js):
    var bubbleApp = angular.module('bubbleApp', ['ngResource'])
  4. $resource and ApiUrl are now available everywhere in the app, so request them in the service

Here's what the service looks like (using JSONP since the API is on another domain):

View

Genres

 How things are displayed isn't really important in this post. Here's what it could look like, with a repeater:

Index.html

More importantly, since we want to display a "Loading" message globally, we will add a <div> straight in the body. The div has a ng-show="loading" directive, so it will show or hide depending on the value of $rootScope loading. The id on the element is only used for CSS purposes. Also note that we add an AppCtrl as the body's controller, it will be created next, and basically becomes the global controller.
Showing the body only:

Touch of CSS

For visual reason, let's just add the following to app/styles/main.css:


Controllers - Version 1: Using $rootScope

GenresCtrl

Using the following controller which request for $rootScope and GenresService, we can set the value of $rootScope.loading to true when landing on the page, and back to false in the complete callback of the $resource.query function:


AppCtrl

Actually not used in version 1, but since we added it to the index.html, Angular will crash if it doesn't exist. So for the time being, let's only bootstrap it:

yeoman init angular:controller app

And that should work. When going to the http://localhost:3051/#/genres url, the controller sets $rootScope.loading to true, and when the loading of $resource is complete, the callback is triggered, putting loading back to false. The loader, which follows the $rootScope.loading value is shown then hidden.
Note that $rootScope could have been accessed from $scope.$root instead.

Controllers - Version 2: Using events

The above works, but it expects GenresCtrl to be aware that the variable "loading" is controlling the loader. Also, if other components will not be informed that we are loading. In any case, this is a perfect use case for events. Angular listens using $on and triggers using $broadcast (top scope down) or $emit (bottom scope up). We decided to emit from the GenresCtrl scope up, bubbling to the AppCtrl:


Conclusion

Using events, we've managed to create a simple "Loading" box, accessible by emitting an event. The loading indicator can therefore be triggered from any part of the app.
Although we didn't include any extra scope, any parent scope of Genres, would've been able to catch the event bubbling towards the App controller. We could have used $rootScope.$broadcast to instead go from the top scope down, maybe in a next post.

No comments:

Post a Comment