How to build a UX-friendly progress indicator with AngularJS

This post demonstrates building an animated progress indicator using AngularJS, and follows user experience (UX) recommendations for visibility of system status. To skip right to the demo, you can check out this JSFiddle.

As one of the original 10 heuristics for web usability, visibility of system status remains one of the most important and universally applicable principles in UI design. With mobile internet usage skyrocketing, more than ever chances are users of your application are accessing functionality over cellular networks with variable bandwidth and performance. Communicating to the user when the application is “busy” loading information is key to keeping users engaged, and reducing task abandonment when they’re on a slow connection, or when the application is doing complicated processing.

A common form of system feedback on websites and applications is an animated progress indicator. This appears when the site is loading or processing information. In modern web application frameworks, such as AngularJS, the client-side application code is typically downloaded all together (i.e. a single page application), so once the application is loaded in the browser, much of the “wait time” is spent making asynchronous requests to web service APIs.

For this demo, we’ll first build a UI for our progress indicator. We’ll keep it simple, and use a overlay to mask out the screen, and a rotating gear icon as our progress indicator.

Rotating Gear Icon

If we use a font-glyph, a gear icon from Font Awesome in this case, we can build this all with HTML and CSS.

HTML:

<div class="indicator-overlay">
  <div class="fa fa-cog fa-spin fa-2x indicator-animation"></div>
</div>

CSS:

div.indicator-overlay {
  height: 100%;
  width: 100%;
  position: absolute;
  z-index: 1000;
  background-color: rgba(200, 200, 200, 0.75);
  margin: -1rem;
  font-size: 4em;
  text-align: center;
}
div.indicator-animation {
  margin: 0 auto;
  width: 100%;
  margin-top: 30%;
}

Next, we’ll create an AngularJS provider, following the “Factory Recipe” to build a common way to show and hide our progress indicator element. Taking this approach, we can use the indicator from anywhere within our AngularJS application.

var myApp = angular.module('MyApp', []);

myApp.factory("busyIndicator", function () {
  return {
    show: function () {
      $(".indicator-overlay").show();
    },
    hide: function () {
      $(".indicator-overlay").hide();
    }
  }
});

Now that we have a way to show and hide our progress indicator, we can tie it into web requests facilitated by the AngularJS $http service. This way, any time we make a request, we can be sure the progress indicator shows the user that something is going on. To accomplish this, we’ll take advantage of $http service Interceptors, which allow pre- and post-processing of web requests and responses. We’ll create another factory to encapsulate our web request busy indicator methods, and then add this to the $http service Interceptors collection.

myApp.factory('requestBusyIndicator', ['$q', 'busyIndicator', '$timeout', function ($q, busyIndicator, $timeout) {
  var requestIndicator = {
    request: function (config) {
      busyIndicator.show();
      return config;
    },
    response: function (response) {
      busyIndicator.hide();
      return response;
    },
    requestError: function (rejection) {
      busyIndicator.hide();
      return $q.reject(rejection);
    },
    responseError: function (rejection) {
      busyIndicator.hide();
      return $q.reject(rejection);
    }
 };
 return requestIndicator;
}]);
myApp.config(['$httpProvider',
   function ($httpProvider) {
     $httpProvider.interceptors.push('requestBusyIndicator');
   }]);

For the sake of the demo, we’ll add a button to our UI to trigger some asynchronous stuff.

<div ng-app="MyApp" ng-controller="MyController">
  <button ng-click="doAsyncStuff()">Click to do something</button>
</div>

And we’ll add a simple AngularJS controller to perform an asynchronous web request.

myApp.controller('MyController', function ($scope, $http) {
  $scope.doAsyncStuff = function () {
    // A real request URI would go here
    $http.post('/echo/json');
  }
});

With the solution so far, any time we make a web request using the $http service, the progress indicator will appear to let the user know the application is busy. Providing this feedback all the time is OK, but we can improve the user experience here. If a request returns quickly, then the user may see a flash of the progress indicator, which could cause confusion and do more harm than good. For this reason, it would be better if we don’t show the progress indicator for really short wait times.

In a blog post about progress indicators, Neilson Norman Group suggests showing a progress indicator if an action takes more than 2 seconds, but that feels a little long to me, so I’m going to code a 1 second (1000 millisecond) delay instead. We could easily change the delayMilliseconds value used in the show method to fine tune the delay.

myApp.factory("busyIndicator", function () {
  return {
    timeout: null,
    delayMilliseconds: 1000,
    indicatorIsVisible: false,
    show: function () {
      if (!this.indicatorIsVisible) {
        this.indicatorIsVisible = true;
        this.timeout = setTimeout(
          function() {
            $(".indicator-overlay").show();
          },
          this.delayMilliseconds);
      }
   },
   hide: function () {
     clearTimeout(this.timeout);
     this.indicatorIsVisible = false;
     setTimeout(
       function() {
       $(".indicator-overlay").hide();
     }, this.delayMilliseconds);
   }
 }
});

Since our response may come back quickly, faster than our 1000 millisecond timeout, we need to make sure that when the progress indicator is hidden that we clear the timeout to prevent the progress indicator being shown after the fact. We also need to check that the progress indicator is not already visible from one request if another request is made in parallel – the indicatorIsVisible guard condition protects from that. We also don’t want the progress indicator to flash too quickly if the request takes just over a second, so we use another timeout in the hide method to ensure the progress animation appears for at least one second.

To test out our UX-friendly progress indicator, we can simulate a response that takes just a little longer than one second by modifying the response method of our requestBusyIndicator interceptor:

...
response: function (response) {
  //Simulate 1100 millisecond response time by deferring the response with a timeout
  var deferred = $q.defer();
  $timeout(function () {
    busyIndicator.hide();
    deferred.resolve(response);
  }, 1100);
  return deferred.promise;
}
...

I’ve used 1100 millisecond here, but this value can be changed to simulate different response times. You can play around with the complete demo in a JSFiddle:

For really long delays – 10 seconds or more – then we would want to consider providing additional feedback, for example, by using some sort of percent-done indicator. However, often times web service response times are difficult to predict, and unless a progress-feedback mechanism is built into the service, it may not be possible to get an accurate estimate of time remaining. The overall user experience may be improved instead by focusing efforts on optimizing the web request/response pattern to ensure that at least the vast majority of responses return quickly.

Leave a Reply

Your email address will not be published.

top