80

This is a two part question:

  1. I am using the resolve property inside $stateProvider.state() to grab certain server data before loading the controller. How would I go about getting a loading animation to show during this process?

  2. I have child states that also utilise the resolve property. The problem is that ui-router seems to want to finalise all resolves before loading any controller. Is there any way I can get the parent controllers to load once their resolves have been resolved, without having to wait for all the child resolves? An answer to this will likely also solve the first problem.

4
  • 4
    Did you ever resolve this issue? Commented Dec 14, 2013 at 23:06
  • 3
    @StefanHenze No, I just accepted this as a ui-router architectural flaw.
    – Matt Way
    Commented Jan 26, 2014 at 0:03
  • 2
    There's a discussion about this exact problem here: github.com/angular-ui/ui-router/issues/456 Commented May 12, 2014 at 20:50
  • 6
    @StefanHenze lol, no pun intended....
    – Dave
    Commented Nov 17, 2014 at 20:54

11 Answers 11

71

EDIT: Here is an even easier solution, tested and working nicely:

In my main controller I simply have

$scope.$on('$stateChangeStart', function(event, toState, toParams, fromState, fromParams) {
    if (toState.resolve) {
        $scope.showSpinner();
    }
});
$scope.$on('$stateChangeSuccess', function(event, toState, toParams, fromState, fromParams) {
    if (toState.resolve) {
        $scope.hideSpinner();
    }
});

This shows the spinner whenever we are about to go to a state that has anything to resolve and hides it, when the state change is complete. You might want to add some check up the state hierarchy (i.e. also show the spinner if a parent state that is being loaded resolves something) but this solution works fine for me.

Here is my old suggestion for reference and as an alternative:

  1. In your application controller, listen to the stateChangeStart event and check if you are about to switch to a state where you want to show a spinner during resolve (see https://github.com/angular-ui/ui-router/wiki/Quick-Reference#wiki-events-1)

    $rootScope.$on('$stateChangeStart', function(event, toState, toParams, fromState, fromParams){
        if (toState.name == 'state.with.resolve') {
            $scope.showSpinner();  //this is a function you created to show the loading animation
        }
    })
    
  2. When you controller finally gets called, you can hide the spinner

    .controller('StateWithResolveCtrl', function($scope) {
        $scope.hideSpinner();
    })
    

You also might want to check for any errors that may have occurred during resolve by listening to the $stateChangeError event and hiding the animation while you handle the error.

This is not totally clean as you distribute the logic for the spinner between controllers, but it's a way. Hope it helps.

4
  • 1
    if (toState.resolve) checks if the state config has a resolve dictionary. In my case, this is a good enough estimate that the state does async service calls. I assume that if there is a resolve block, it's service calls that get resolved. The code shows and hides the spinner only if service calls were made. As I described above, this code might need to be refined to also check parent states, depending on your use case. Commented May 8, 2014 at 18:20
  • in the 'old suggestion' you reference $scope inside a call on $rootScope. where does the $scope come from here?
    – pdeva
    Commented Jun 4, 2014 at 18:01
  • @pdeva, the event handler was defined inside some controller; that's where also the showSpinner function was defined. Commented Jun 5, 2014 at 16:06
  • I couldn't use toState.resolve so I used fromState.name !== toState.name. Aside from that your answer is fantastic, well done! Commented Feb 14, 2019 at 5:17
22

I developed the following solution which works perfectly for me.

1. Add the following app.run

app.run(function($rootScope){

    $rootScope
        .$on('$stateChangeStart', 
            function(event, toState, toParams, fromState, fromParams){ 
                $("#ui-view").html("");
                $(".page-loading").removeClass("hidden");
        });

    $rootScope
        .$on('$stateChangeSuccess',
            function(event, toState, toParams, fromState, fromParams){ 
                $(".page-loading").addClass("hidden");
        });

});

2. Place the loading indicator just above the ui-view. Add id="ui-view" to ui-view div.

<div class="page-loading">Loading...</div>
<div ui-view id="ui-view"></div>

3. add the following to your css

.hidden {
  display: none !important;
  visibility: hidden !important;
}

NOTE:

A. The above code will display loading indicator in two cases 1) when the angular app is loaded first time 2) when the view is changed.

B. If you don't want the indicator to be shown when the angular app loads first time (before any view is loaded), then add hidden class to the loading div like below

<div class="page-loading hidden">Loading...</div>
5
  • 15
    That run code is soo not the Angular Way. Instead of manipulating the DOM there, set scope variables like loadingVisible and viewVisible, and then in the HTML, imperatively say when the elements are visible or hidden, like: <div class="page-loading" ng-show="viewVisible">Loading...</div>. For emptying the view it may require a custom directive though.
    – Matthias
    Commented Jun 19, 2015 at 17:10
  • 2
    This is NOT recommended. Using jQuery inside angularcontext is nasty and is creating an unwanted dependency between your view and controller - not good practice! Commented Dec 4, 2015 at 9:17
  • I like this but ... Is it really correct in relation to the DOM?
    – oshliaer
    Commented Jan 7, 2016 at 5:03
  • The issue with doing this 'the angular way' like @MatthiasDailey suggests is that you'd need to duplicate the show/hide logic all over the place. The neat thing about this solution is that you can have a global method to show/hide the content or loaders. If you'd really want to do this the Angular way, then make a new directive that wraps ui-view
    – thomaux
    Commented Mar 9, 2016 at 8:03
  • 1
    I think ng-class would be a better approach to this.
    – UserX
    Commented Apr 7, 2016 at 0:01
16

I've found using the angular-loading-bar worked really well for long resolves due to network access.

0
8

How about adding content to the div that will be filled by ui-router once the properties have resolved?

In your index.html

<div ui-view class="container">
    Loading....
</div>

The user will now see "Loading..." while the properties are resolved. Once everything is ready the content will be replace by ui-router with your apps content.

4
  • 7
    This would work if your application can have a full app loading replacement, but falls short if you want to build 'loading...' hierarchically into your layout (eg. having part of your application show, with another part displaying loading...).
    – Matt Way
    Commented Dec 9, 2013 at 6:00
  • 1
    It's still a quick/dirty/easy way to get you moving on to more important things... like speeding up your resolves. Commented Mar 6, 2014 at 10:28
  • 3
    If you are loading multiple templates into that view, which ideally you would want to. The "loading...." message would appear only for the first time. Commented Jan 7, 2015 at 8:21
  • Is this possible in Ionic? I was not able to use this way in Ionic. Commented Jun 21, 2016 at 13:31
7

I use an animated gif that I show only when $http has pending requests.

In my base page template, I have a navbar and navbar controller. The relevant part of the controller looks like this:

controllers.controller('NavbarCtrl', ['$scope', '$http',
    function ($scope, $http) {
        $scope.hasPendingRequests = function () {
            return $http.pendingRequests.length > 0;
        };
    }]);

The corresponding code in my html is:

<span class="navbar-spinner" ng-show="hasPendingRequests()">
    <img src="/static/img/spinner.gif">
</span>

I hope that helps!

2
  • 3
    As you can see by my question, I don't have any access to controllers until all resolves have been finalised (which is the problem)!
    – Matt Way
    Commented Nov 25, 2013 at 21:55
  • 1
    You shouldn't use function for ng-show or ng-if, because they will be called in each digest loop. It will decrease performance. Commented Apr 20, 2017 at 0:09
4

My idea is to walk the path on state graph between transitioning states on $stateChangeStart and collect all involved views. Then every ui-view directive watches if corresponding view is involved in transition and adds 'ui-resolving' class on it's element.

The plunker demo introduces two root states: first and second, the latter has two substates second.sub1 and second.sub2. The state second.sub2 also targets footer view that belongs to its grandparent.

3

This is loader for globally when page navigate between any state (any page), put in app.js

.run(
    ['$rootScope',
        function($rootScope) {
            $rootScope.$on('$stateChangeStart', function(event, toState, toParams, fromState, fromParams) {
                $rootScope.preloader = true;
            })
            $rootScope.$on('$stateChangeSuccess', function(event, toState, toParams, fromState, fromParams) {
                $rootScope.preloader = false;
            })
        }
    ])

In html:

<div ng-show="preloader">Loading...</div>
1
  • Couldn't be more obvious... maybe explain that how do you put something into the ui-view while the loading is in progress? Defining 2 variables is so trivial Commented Apr 23, 2017 at 0:03
3

The use of $stateChangeStart and the like have since been deprecated and replaced with Transition Hooks. So, for the answer by Stefan Henze, the updated version would be:

$transitions.onStart({}, function(transition) {
  if (transition.to().resolve) {
    $scope.showSpinner();
  }
});

$transitions.onSuccess({}, function(transition) {
  if (transition.to().resolve) {
    $scope.hideSpinner();
  }
});

You can use this in you parent controller. Remember to inject $transitions -

.controller('parentController',['$transitions',function($transitions){...}]);

Also, keep in mind that a resolve that is an empty object will still render transition.to().resolve == true, so don't leave an empty placeholder resolve in the state declaration.

2

I prefer using a directive to match any loading activity, mainly to keep my codebase clean

angular.module('$utilityElements', [])
.directive('loader',['$timeout','$rootScope', function($timeout, $rootScope) {
    return {
      restrict: 'A',
      template: '<div id="oneloader" class="hiddenload">loading...</div>',
      replace: true,
      compile: function (scope, element, attrs) {
        $timeout(function(){
          $rootScope
              .$on('$stateChangeStart',
                  function(event, toState, toParams, fromState, fromParams){
                      $("#oneloader").removeClass("hiddenload");
              });

          $rootScope
              .$on('$stateChangeSuccess',
                  function(event, toState, toParams, fromState, fromParams){
                      //add a little delay
                      $timeout(function(){
                        $("#oneloader").addClass("hiddenload");
                      },500)
              });
        }, 0);
      }
    }
  }]);
1

My Idea to use view while using resole in router it is working awesome. try this.

//edit index.html file 
<ion-nav-view>
    <div ng-show="loadder" class="loddingSvg">
        <div class="svgImage"></div>
    </div>
</ion-nav-view>

// css file

.loddingSvg {
    height: 100%;
    background-color: rgba(0, 0, 0, 0.1);
    position: absolute;
    z-index: 99;
    left: 0;
    right: 0;
}

.svgImage {
    background: url(../img/default.svg) no-repeat;
    position: relative;
    z-index: 99;
    height: 65px;
    width: 65px;
    background-size: 56px;
    top: 50%;
    margin: 0 auto;
}

// edit app.js

 .run(function($ionicPush, $rootScope, $ionicPlatform) {




        $rootScope.$on('$stateChangeStart',
            function(event, toState, toParams, fromState, fromParams) {
                $rootScope.loadder = true;
            });

        $rootScope.$on('$stateChangeSuccess',
            function(event, toState, toParams, fromState, fromParams) {
                $rootScope.loadder = false;
            });

});
0

If anyone is using ngRoute, waiting on resolve before loading the next view, and using angular-bootstrap-ui for ui, you can do the following:

app.config([
  "$routeProvider", "$locationProvider", function($routeProvider, $locationProvider) {
    return $routeProvider.when("/seasons/:seasonId", {
      templateUrl: "season-manage.html",
      controller: "SeasonManageController",
      resolve: {
        season: [
          "$route", "$q", "$http", "$modal", function($route, $q, $http, $modal) {
            var modal, promise, seasonId;
            modal = $modal.open({
              backdrop: "static",
              template: "<div>\n  <div class=\"modal-header\">\n    <h3 class=\"modal-title\">\n      Loading...\n    </h3>\n  </div>\n  <div class=\"modal-body\">\n    <progressbar\n      class=\"progress-striped active\"\n      value=\"'100'\">\n    </progressbar>\n  </div>\n</div>",
              keyboard: false,
              size: "lg"
            });
            promise = $q.defer();
            seasonId = $route.current.params.seasonId;
            $http.get("/api/match/seasons/" + seasonId).success(function(data) {
              modal.close();
              promise.resolve(data);
            }).error(function(data) {
              modal.close();
              promise.reject(data);
            });

            return promise.promise;
          }
        ]
      }
    });
  }
]);

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.