Search code examples
javascriptangularjsangularjs-ng-route

Show loading indicator at root level until all route resolves are complete


I've been doing some Googling around this already but I'm unable to find a solution that works.

I'm using AngularJS 1.5.5 and .NET Web API 2 to build a web application and I would quite simply like to hide the ng-view element until all resolves have completed on the route.

I'm trying to use the $routeChangeStart and $routeChangeSuccess to set a variable on the $rootScope that is used in the index html to display the loading indicator and hide the content until the variable is false.

Here is my routing code for the routeChange properties:

_app.config([
        '$routeProvider', '$httpProvider', '$provide',
        function ($routeProvider, $httpProvider, $provide) {
                    $routeProvider.when('/Account',
                    {
                        templateUrl: '/Content/js/areas/account/account.html',
                        controller: 'accountController',
                        resolve: {
                            $accountResolver: function (accountService) {
                                return accountService.getMyAccountData();
                            }
                        },
                        caseInsensitiveMatch: true
                    });
            $routeProvider.otherwise({ redirectTo: '404' });

        }
]);

_app.run(['$rootScope', '$location', '$window', '$q', 'authService',
        function ($rootScope, $location, $window, $q, authService) {
            $rootScope.$on("$routeChangeStart",
                function (e, curr, prev) {
                    $rootScope.$loadingRoute = true;
                });
            $rootScope.$on("$routeChangeSuccess",
                function (evt, next) {
                    $rootScope.$loadingRoute = false;
                });
            $rootScope.$on("$routeChangeError",
                function (evt, next) {
                    $rootScope.$loadingRoute = false;
                });
        }]);

And here is my html using that $loadingRoute variable:

<body class="ng-cloak" data-ng-app="wishlist" data-ng-controller="appController">
    <wl-header></wl-header>
    <preloader ng-if="$loadingRoute"></preloader>
    <section ng-view ng-if="!$loadingRoute" class="container ng-cloak"></section>
</body>

I understand that there's quite a lot of articles covering this but none seem to work in my case. $loadingRoute gets set to true when the route change starts, as expected, which I will see if I add {{$loadingRoute}} to the HTML before the <section></section> tag. However before the $accountResolveris resolved, the $routeChangeSuccess gets fired, setting $rootScope.$loadingRoute = false which is unexpected.

I was under the impression that $routeChangeSuccess only got fired after all resolves had completed on the current route.

Am I doing something really obviously wrong here? Or has Angular simply changed?

Edit: I would also like to add that this approach worked in previous projects, so I'm at a real loss as to what's going wrong. I could set $rootScope.$loadingRoute manually in each page controller but that feels too dirty and unmaintainable.

Edit 2:

_app.factory('accountService', [
        'accountResource',
        function (accountResource) {
            var _self = this;

            return {
                register: function (authData) {
                    return accountResource.register(authData);
                },

                getMyAccountData: function () {
                    return accountResource.getMyAccountData();
                }
            }
        }
]);

_app.factory('accountResource', [
        '$resource', 'rootUrl',
        function ($resource, rootUrl) {
            var api = rootUrl() + 'api/Account';
            return $resource(api,
            {},
            {
                register: {
                    method: 'POST',
                    url: '{0}/register'.format(api)
                },
                getMyAccountData: {
                    method: 'GET',
                    url: '{0}/GetMyAccountData'.format(api)
                }
            });
        }
    ])

Solution

  • In order for a resolver to delay route change, it should return a promise. Otherwise route change happens immediately, this is what happens when $routeChangeSuccess is triggered before a promise from accountService.getMyAccountData() is resolved.

    The problem is $resource methods (and so accountService.getMyAccountData()) return self-filling object that is populated with data asynchronously. A promise for this data is available as $promise property (see the reference), so it should be used for a resolver:

    $accountResolver: function (accountService) {
      return accountService.getMyAccountData().$promise;
    }
    

    If accountService is supposed to be purely promise-based wrapper for accountResource, a cleaner way to do this is to return a promise from its methods instead:

    getMyAccountData: function () {
        return accountResource.getMyAccountData().$promise;
    }