angularJS Button directive with busy indicator

In my previous post, I was experimenting on creating an overall “busy” indicator for a current page. The KoLite helper has a very good feature implementation for buttons (and more) where it can show an indicator that an operation is going on.

I thought of augmenting angularJS’s ngClick  directive to implement a similar feature.

Normally, we would declare an ngClick directive if we want to implement some behavior/process when we click the button.

<button type="submit" data-ng-click="vm.getComments()">Get Comments</button>

But if the operation takes  a long time, we have no visual clue (to the user) of what’s going on.


We want to change that! In my previous post, I used NETEYE activity indicator. This time, I will use spin.js (by the same author). What prompted me to use spin.js is that it doesn’t rely on jQuery anymore.

So how do we go about this? Basically,we need to create a directive that somehow:

  • Handles the click event
  • Displays the spinner upon the start of the process
  • Disables the button
  • Calls the bound function for the event (returns a promise)
  • Removes the spinner when done
  • Enables the button

The key thing to point out here is that the bound function needs to return a promise. For example, our markup would look like this now (with our new directive)

<button type="submit" data-indi-click="vm.getComments()">Get Comments</button>

where getComments() is a function in your Controller $scope that returns a promise. For example

$scope.comments = [];
$scope.getComments = function () {
    $scope.comments = getDataQ(); // call ajax and "magically" bind it to our array
    return $scope.comments; // need to return this promise

function getDataQ() {
    var deferred = $q.defer();
    $http({ method: 'GET', url: '/api/QTest/GetAll' })
    .success(function (data) {
    return deferred.promise;

Or, for clarity we could also write getComments() as :

$scope.getComments = function () {
    var t = getDataQ(); // call ajax

    t.then(function (results) {
        $scope.comments = results;
    return t; // need to return this promise

The directive

(function (Spinner) {
    'use strict';
    var directiveId = 'indiClick';
    app.directive(directiveId, ['$parse', function ($parse) {
        var directive = {
            link: link,
            restrict: 'A'
        return directive;
        function link(scope, element, attr) {
            var fn = $parse(attr[directiveId]), // "compile" the bound expression to our directive
            target = element[0],
            height = element.height(),
            oldWidth = element.width(),
            opts = {
                length: Math.round(height / 3),
                radius: Math.round(height / 5),
                width: Math.round(height / 10),
                color: element.css("color"),
                left: -5
            }; // customize this "resizing and coloring" algorithm

            // implement our click handler
            element.on('click', function (event) {
                scope.$apply(function () {
                    attr.$set('disabled', true);
                    element.width(oldWidth + oldWidth / 2); // make room for spinner

                    var spinner = new Spinner(opts).spin(target);
                    // expects a promise
                    fn(scope, { $event: event })
                    .then(function (res) {
                        element.width(oldWidth); // restore size
                        attr.$set('disabled', false);
                        return res;
                    }, function (res) {
                        element.width(oldWidth); // restore size
                        attr.$set('disabled', false);

With this implementation, our button will now be more user friendly, as it gives clues to what’s going on…


Head over to Github to fork out the code.

Disclaimer: The opinions expressed herein are my own personal opinions and do not represent my employer’s view in any way.
Tagged with: , , ,
Posted in angularJS
  • ericpanorel

    Note that this code breaks when using RC2. Use RC1 for now. See related issue here:

  • ronnie

    Why do I get “Uncaught ReferenceError: Spinner is not defined “??
    ‘Spinner’ is not defined in your directive