Tagged: directive

Waiting indicator using AngularJS interceptors

There are a number of needs that routinely come up when putting together a web UI.   This indicator notifies the user that action is occurring in the background, and goes away automatically when everything is done.
please wait
This happens automatically when your AngularJS app is requesting information via http. This is similar to other code on the web, but is modified it to suit the following needs:

  • Displays wait indicator automatically when http requests are initiated.
  • Tracks the number of requests, and dismisses the indicator when all are resolved.
  • A delay of one second prevents indicator from displaying during angular loading views and quick requests.
  • No jquery (yay)

Here it is on JSFiddle

If you’re looking for code (and you probably are), I recommend taking a look at the JSFiddle link above – it integrates the code, css and html.

We’re using several key AngularJS features here:

Directive

The directive is Angular’s way to hook UI elements to javascript functionality. Much of the logic is located here.

myApp.directive("loadingIndicator", function (loadingCounts, $timeout) {
    return {
        restrict: "A",
        link: function (scope, element, attrs) {
            scope.$on("loading-started", function (e) {
                loadingCounts.enable_count++;
                console.log("displaying indicator " + loadingCounts.enable_count);
                //only show if longer than one sencond
                $timeout(function () {
                    if (loadingCounts.enable_count > loadingCounts.disable_count) {
                        element.css({ "display": "" });
                    }
                }, 1000);
            });
            scope.$on("loading-complete", function (e) {
                loadingCounts.disable_count++;
                console.log("hiding indicator " + loadingCounts.disable_count);
                if (loadingCounts.enable_count == loadingCounts.disable_count) {
                    element.css({ "display": "none" });
                }
            });
        }
    };
});

Interceptors

Allows us to tie into http-related events and run our code at that time. In this case we are hooking into both the request, response and responseError (just in case). Here’s another post on interceptors.

myApp.config(function ($httpProvider) {
    $httpProvider.interceptors.push(function ($q, $rootScope) {
        return {
            'request': function (config) {
                $rootScope.$broadcast('loading-started');
                return config || $q.when(config);
            },
            'response': function (response) {
                $rootScope.$broadcast('loading-complete');
                return response || $q.when(response);
            },
            'responseError': function (rejection) {
                $rootScope.$broadcast('loading-complete');
                return $q.reject(rejection);
            }
        };
    });
});

Broadcast

Broadcast is the way we can fire (and respond to) our own events. As you can see, the interceptor events use $broadcast messages to trigger the logic (located in the directive) to show or hide the indicator.

You can implement this solution from the code on this page, if you include the following:

myApp.factory('loadingCounts', function () {
    return {
        enable_count: 0,
        disable_count: 0
    }
});

See it in action: Here it is on JSFiddle

SharePoint App Part Resizing, When Using AngularJS

Provider Hosted Apps

Sometimes it’s necessary to embed rich, non-SharePoint functionality in a SharePoint site. The preferred way to do this in SharePoint 2013 is to use a Provider Hosted App. SharePoint then acts as a “host” for the app, which can function independently. This gives the ability to layer in complex business rules, perform development cycles AWAY from SharePoint, and then embed the functionality in the site.

IFrame Presentation

Taking this route, one challenge is to ensure that the app appears as part of the page. Provider Hosted Apps are hosted in an iframe, but users need not be aware of this fact. If not done properly, parts of the app might disappear or for scroll bars may appear in the frame. Generally the iframe content cannot control the size of the “parent” window.

So it’s up to the developer to ensure that the iframe is sized properly for the app. Fortunately, Microsoft has anticipated this issue and given us some javascript “hooks” to do this. There are a number of blogs on how to achieve this, from simple implementation to complex ones . Bottom line, your hosted javascript code needs to send a message similar to this one to the parent window:

<message senderId=12345ABCD>resize(100%,800px)</message>

Additional Challenges with AngularJS

Resizing the parent iFrame is a simpler task with a “traditional” (mostly server side) app – then page size changes when the page reloads, and the necessary logic can be run then.

But with AngularJS (a single page app), the iframe needs to resize when we move from one “view” to another, or the page size changes due to the underlying javascript logic. When resizing, we need to know what the size of our content will be. Fortunately, this is not rocket science – it just requires a “harmonic convergence” of code.

Step 1: Add wrapper HTML

Identify or create a wrapper div which will contain all HTML in your app. We will check this wrapper to determine how large the iframe should be.

<body ng-app="appName">
    <div main-wrapper>
        <!-- Add your site or application content here -->
        <div ng-view=""></div>
    <div>
</body>

Step 2: Add Resizing Code

Add some javascript code to resize the iframe. The following code assumes that only the height of your app will vary. Note the chrome fix – this is another issue that applies only to Angular implementations.

"use strict";
function adjustFrameSize(contentHeight) {
    var senderId,
        resizeMessage = '<message senderId={Sender_ID}>resize({Width}, {Height}px)</message>';

    var args = document.URL.split("?");
    if (args.length < 2) return;
    var params = args[1].split("&");
    for (var i = 0; i < params.length; i = i + 1) {
        var param = params[i].split("=");
        if (param[0].toLowerCase() == "senderid") {
            senderId = decodeURIComponent(param[1]);
            senderId = senderId.split("#")[0]; //for chrome - strip out #/viewname if present
        }
    }

    var step = 30, finalHeight;
    finalHeight = (step - (contentHeight % step)) + contentHeight;

    resizeMessage = resizeMessage.replace("{Sender_ID}", senderId);
    resizeMessage = resizeMessage.replace("{Height}", finalHeight);
    resizeMessage = resizeMessage.replace("{Width}", "100%");
    console.log(resizeMessage);
    window.parent.postMessage(resizeMessage, "*");
}

Step 3: Add an Angular Directive

In your AngularJS code, create a directive which will attach to your wrapper div. What you’re doing here is creating an event which can be fired at any time from your app, to trigger a resize of the iframe based on the height of your wrapper div.

  .directive('mainWrapper', function ($timeout) {
        return {
            restrict: 'A',
            link: function (s, e, attrs) {
                s.$on('resizeframe', function () {
                    $timeout(function () {
                        //timeout ensures that it's run after the DOM renders.
                        adjustFrameSize(e[0].offsetHeight);
                    }, 0, false);
                });
            }
        };
    })

Step 4: Trigger the Resizing Event

Finally, you need to instruct the event to fire at the appropriate times. This might be when a controller is intially accessed, or perhaps when data are accessed. The strength of this method is that you can control when resizing occurs. You need to inject $scope and use $broadcast. Here are a couple of examples:

.controller('FirstPageControler', function ($scope) {
      //resize frame when controller loads
      $scope.$parent.$broadcast('resizeframe');
})

.controller('AnotherControler', function ($scope, webServices) {
      webServices.reports().then(function (response) {
          $scope.reports = response;

          //resize frame upon successful return from a service.
          //presumably we have returned with data that will resize the page.
          $scope.$parent.$broadcast('resizeframe');
      }, function (response) {
          $scope.reports = response;
      });
})

That should do the job! Good luck, and feel free to leave a comment if you’ve found a better way.