Web developers looking to offer a native app feel to their sites’ visitors will most likely consider using the single page app (SPA) approach. SPAs allow for a snappy user experience because redrawing parts of the page does not necessarily require a request to be made to the server. That said, the need occasionally does arise to make API calls from the single page app, and in such cases the app should show some indication to let the user know that something is happening in the background.
This post will discuss one approach to add an activity indicator to your AngularJS app. Of course, there are countless other ways to achieve the same result, and you will also find modules that can help you with this need.
How to Add an Activity Indicator to Your AngularJS App
Let’s start by adding the HTML markup for the activity indicator. This could live in a separate template or in the main page of the app (often index.html). Because the activity indicator normally has a simple structure and because it will need to be loaded anyway, I will put it in the main page in this example. In the code snippet below, the indicator just consists of one <div> tag, showing the word “Loading…”.
[...] <body ng-app="myApp"> <div class="progress-indicator" ng-controller="ProgressIndicatorCtrl" ng-show="isLoading"> Loading... </div> </body>
You will want different appearance on a project-by-project basis, so I will not include too much styling in the example. The ng-controller attribute specifies the name of the Angular controller that will be loaded to manage the scope of this element. The controller will have the following implementation:
[...] .controller('ProgressIndicatorCtrl', ['$scope', function($scope) { // Keeps track of active tasks. var loadingTasks = []; // Used in the template to show/hide the indicator. $scope.isLoading = false; /** * Listener for the 'startedProgress' event. * Registers an active task. */ $scope.$on('startedProgress', function (event, task) { // Add the task to the list. loadingTasks.push(task); // Show the indicator. $scope.isLoading = true; }); /** * Listener for the 'finishedProgress' event. * Removes an active task. */ $scope.$on('finishedProgress', function (event, task) { // Remove the task from the list of active tasks. var index = loadingTasks.indexOf(task); if (index != -1) { loadingTasks.splice(index, 1); } // If nothing else is in progress, hide the indicator. if (loadingTasks.length == 0) { $scope.isLoading = false; } }); /** * Listener for the 'resetProgressIndicator' event. * Hides the indicator and resets the list of active tasks. */ $scope.$on('resetProgressIndicator', function (event, task) { loadingTasks = []; $scope.isLoading = false; }); }]);
The controller has a local variable, loadingTasks, which will keep track of the tasks that are currently loading. I have chosen this approach instead of just keeping a count of the active tasks for the following reason: I need to be able to cancel the indicator when the user acts while a task is in progress. To give you a use-case example, let’s say the user starts some process, then takes an action that makes that process irrelevant. In such cases, the indicator needs to be hidden, while the actual process may still be pending. When the process finally completes in the background, it may decrease the task counter, which has already been reset by the user taking action. This could throw off the counter and lead to unexpected behavior.
The controller then adds event listeners for the “startedProgress”, “finishedProgress”, and “resetProgressIndicator” events. These events are expected to be broadcast by other controllers that start the time consuming tasks. The task argument in each event is expected to be a string that represents the task being run. This is used to identify the task.
On each “startedProgress” event, the task’s name is added to the array of active tasks. The isLoading variable is then set to true. This variable is used in the template as a condition in ng-show to show and hide the activity indicator. The “finishedProgress” event removes the given task from the list, and hides the indicator if there are no more active tasks. The “resetProgressIndicator” event should be broadcast when the indicator needs to be reset, that is hidden, despite any ongoing tasks.
After having the indicator in place, we can start using it as follows. When a time consuming task is started from anywhere in the app, the app needs to broadcast the “startedProgress” event from the root scope as shown below.
$rootScope.$broadcast('startedProgress', 'myTaskName');
When the task is done, you’ll need to broadcast the “finishedProgress” event.
$rootScope.$broadcast('finishedProgress', 'myTaskName');
When the user takes an action that renders the currently running tasks irrelevant, broadcast the "resetProgressIndicator” event.
$rootScope.$broadcast(‘resetProgressIndicator');
Assuming these events are being broadcast at the appropriate places, the indicator should consistently appear to notify the user of each time consuming task. See the attached file for an example app that demonstrates how to use the activity indicator.