322

I am wondering if there is a way (similar to Gmail) for AngularJS to delay showing a new route until after each model and its data has been fetched using its respective services.

For example, if there were a ProjectsController that listed all Projects and project_index.html which was the template that showed these Projects, Project.query() would be fetched completely before showing the new page.

Until then, the old page would still continue to show (for example, if I were browsing another page and then decided to see this Project index).

kryger
  • 12,906
  • 8
  • 44
  • 65
Misko Hevery
  • 47,936
  • 11
  • 40
  • 36

13 Answers13

374

$routeProvider resolve property allows delaying of route change until data is loaded.

First define a route with resolve attribute like this.

angular.module('phonecat', ['phonecatFilters', 'phonecatServices', 'phonecatDirectives']).
  config(['$routeProvider', function($routeProvider) {
    $routeProvider.
      when('/phones', {
        templateUrl: 'partials/phone-list.html', 
        controller: PhoneListCtrl, 
        resolve: PhoneListCtrl.resolve}).
      when('/phones/:phoneId', {
        templateUrl: 'partials/phone-detail.html', 
        controller: PhoneDetailCtrl, 
        resolve: PhoneDetailCtrl.resolve}).
      otherwise({redirectTo: '/phones'});
}]);

notice that the resolve property is defined on route.

function PhoneListCtrl($scope, phones) {
  $scope.phones = phones;
  $scope.orderProp = 'age';
}

PhoneListCtrl.resolve = {
  phones: function(Phone, $q) {
    // see: https://groups.google.com/forum/?fromgroups=#!topic/angular/DGf7yyD4Oc4
    var deferred = $q.defer();
    Phone.query(function(successData) {
            deferred.resolve(successData); 
    }, function(errorData) {
            deferred.reject(); // you could optionally pass error data here
    });
    return deferred.promise;
  },
  delay: function($q, $defer) {
    var delay = $q.defer();
    $defer(delay.resolve, 1000);
    return delay.promise;
  }
}

Notice that the controller definition contains a resolve object which declares things which should be available to the controller constructor. Here the phones is injected into the controller and it is defined in the resolve property.

The resolve.phones function is responsible for returning a promise. All of the promises are collected and the route change is delayed until after all of the promises are resolved.

Working demo: http://mhevery.github.com/angular-phonecat/app/#/phones Source: https://github.com/mhevery/angular-phonecat/commit/ba33d3ec2d01b70eb5d3d531619bf90153496831

Mike Chamberlain
  • 39,692
  • 27
  • 110
  • 158
Misko Hevery
  • 47,936
  • 11
  • 40
  • 36
  • In example you use $defer. Is $defer was replaced by $timeout? As far as I understand from example it delays route change not until data is loaded, but on 1 sec. And we can only hope that data will be loaded for this second... – Artem Andreev Aug 16 '12 at 04:10
  • the defer/delay is only used as an example. It delays at least on second or until data lodade. – Misko Hevery Aug 21 '12 at 23:45
  • 1
    the way to think about is that all of the promises must be resolved before the page continues. – Misko Hevery Aug 21 '12 at 23:46
  • Misko, how can I go about creating a promise that waits until a variable in $scope changes to what I care about? I tried, but it doesn't seem that the promises have access to the $scope variable? – dougvk Aug 23 '12 at 04:37
  • 3
    @MiskoHevery Thanks, but I can't fully understand why resolve.phones returns a promise. As far as I know from [documentation](http://docs.angularjs.org/api/ngResource.$resource#Returns) Phone.query() returns empty array which will be populated with the actual data when data is returned from the server. – Artem Andreev Aug 23 '12 at 06:46
  • 10
    @MiskoHevery - what if your controllers are inside a module and are defined as a string rather than function. How could you setup the resolve attribute like you do? – aar0n Oct 03 '12 at 22:29
  • 53
    How is this used in `angular.controller()` type controller definitions? In the `$routeProvider` stuff, I thought you had to use string names of controllers. – Ben Lesh Nov 09 '12 at 14:11
  • @MiskoHevery How do I pass in the value if the controller is using an `$inject' to inject services into the controller? – rxgx Jan 17 '13 at 06:02
  • Tried to use this - and the promises from $resource are NOT resolved when the controller starts; promises done using $q are resolved though. – ondra Jan 27 '13 at 17:54
  • 1
    Read here regarding $resource and promises - and why the above doesn't work as expected: https://groups.google.com/forum/?fromgroups=#!topic/angular/DGf7yyD4Oc4 – georgiosd Jan 28 '13 at 12:10
  • 6
    Any example using angular.controller() and with the latest version of AngularJS? – Laurent Mar 07 '13 at 06:13
  • 1
    For those interested in the minification friendly angular.controller() style, I just wrote a new answer below. I hope it's helpful. Cheers. – bitwit Mar 14 '13 at 19:19
  • 22
    @blesh, when you use `angular.controller()`, you can assign result of this function to a variable (`var MyCtrl = angular.controller(...)`) and then work with that further (`MyCtrl.loadData = function(){..}`). Check out egghead's video, the code is shown there straight away: http://www.egghead.io/video/0uvAseNXDr0 – petrkotek Apr 21 '13 at 02:33
  • 2
    @beret good point, I guess because of hoisting and the fact the controller would be a reference type, that is all you'd have to do. It's so obvious, I feel silly having asked the above question. +1 to you for pointing out the obvious. haha. – Ben Lesh Apr 22 '13 at 03:14
  • $defer has been renamed to $timeout see: https://groups.google.com/d/msg/angular/IxYes7wyYQA/M3PpOMyKxZYJ – Michael Aug 21 '13 at 15:06
  • I'm having problems switching back and forth to a page and still having resolve called. Is that possible? – jbenowitz Aug 22 '13 at 20:18
  • 17
    I'd still like a nice way to do with without having place your controller in a global. I don't want to litter with globals all over the place. You can do it with a constant, but it would be nice to be able to put the resolve function on/in the controller, not somewhere else. – Erik Honn Sep 13 '13 at 14:22
  • @ErikHonn Wrap your Angular source files in a self executing function so you don't have to worry about mucking up your global scope. – Roy Daniels Oct 19 '13 at 15:41
  • I don't run other js than Angular, so that would still make it global for my entire project :P – Erik Honn Oct 21 '13 at 08:09
  • The delay is not required right? And can this be updated with the new $timeout method? – CMCDragonkai Nov 02 '13 at 04:07
  • 3
    Assigning it to app.controller result is not right, as the result returns the same object. `a = angular.module('a', []); a.controller(function () {}) == a.controller(function (){})` so it won't work with multiple controllers. – elado Feb 21 '14 at 08:14
  • How can I cancel route navigation if I got rejected promise? – Gangadhar Jannu Feb 01 '16 at 08:56
  • @GangadharJannu Listen for `$routeChangeError` – Taku Aug 28 '16 at 06:58
51

Here's a minimal working example which works for Angular 1.0.2

Template:

<script type="text/ng-template" id="/editor-tpl.html">
    Editor Template {{datasets}}
</script>

<div ng-view>

</div>

JavaScript:

function MyCtrl($scope, datasets) {    
    $scope.datasets = datasets;
}

MyCtrl.resolve = {
    datasets : function($q, $http) {
        var deferred = $q.defer();

        $http({method: 'GET', url: '/someUrl'})
            .success(function(data) {
                deferred.resolve(data)
            })
            .error(function(data){
                //actually you'd want deffered.reject(data) here
                //but to show what would happen on success..
                deferred.resolve("error value");
            });

        return deferred.promise;
    }
};

var myApp = angular.module('myApp', [], function($routeProvider) {
    $routeProvider.when('/', {
        templateUrl: '/editor-tpl.html',
        controller: MyCtrl,
        resolve: MyCtrl.resolve
    });
});​
​

http://jsfiddle.net/dTJ9N/3/

Streamlined version:

Since $http() already returns a promise (aka deferred), we actually don't need to create our own. So we can simplify MyCtrl. resolve to:

MyCtrl.resolve = {
    datasets : function($http) {
        return $http({
            method: 'GET', 
            url: 'http://fiddle.jshell.net/'
        });
    }
};

The result of $http() contains data, status, headers and config objects, so we need to change the body of MyCtrl to:

$scope.datasets = datasets.data;

http://jsfiddle.net/dTJ9N/5/

Morteza Tourani
  • 3,506
  • 5
  • 41
  • 48
mb21
  • 34,845
  • 8
  • 116
  • 142
  • I'm trying to do something like this but having trouble with injecting 'datasets' as it is not defined. Any thoughts? – Rob Bygrave Jan 08 '13 at 10:50
  • Hey mb21, I think you might be able to help me out with this question: http://stackoverflow.com/questions/14271713/multiple-promises-from-single-http-request-in-angularjs – winduptoy Jan 11 '13 at 04:33
  • Could someone help me convert this answer to the app.controller('MyCtrl') format? http://jsfiddle.net/5usya/1/ didn't work for me. – user1071182 Apr 15 '13 at 03:03
  • i get an error: `Unknown provider: datasetsProvider <- datasets` – chovy Nov 25 '13 at 06:32
  • You can make your answer simpler by replacing datasets with this: `function($http) { return $http({method: 'GET', url: '/someUrl'}) .then( function(data){ return data;}, function(reason){return 'error value';} ); }` – Morteza Tourani May 08 '16 at 20:29
32

I see some people asking how to do this using the angular.controller method with minification friendly dependency injection. Since I just got this working I felt obliged to come back and help. Here's my solution (adopted from the original question and Misko's answer):

angular.module('phonecat', ['phonecatFilters', 'phonecatServices', 'phonecatDirectives']).
  config(['$routeProvider', function($routeProvider) {
    $routeProvider.
      when('/phones', {
        templateUrl: 'partials/phone-list.html', 
        controller: PhoneListCtrl, 
        resolve: { 
            phones: ["Phone", "$q", function(Phone, $q) {
                var deferred = $q.defer();
                Phone.query(function(successData) {
                  deferred.resolve(successData); 
                }, function(errorData) {
                  deferred.reject(); // you could optionally pass error data here
                });
                return deferred.promise;
             ]
            },
            delay: ["$q","$defer", function($q, $defer) {
               var delay = $q.defer();
               $defer(delay.resolve, 1000);
               return delay.promise;
              }
            ]
        },

        }).
      when('/phones/:phoneId', {
        templateUrl: 'partials/phone-detail.html', 
        controller: PhoneDetailCtrl, 
        resolve: PhoneDetailCtrl.resolve}).
      otherwise({redirectTo: '/phones'});
}]);

angular.controller("PhoneListCtrl", [ "$scope", "phones", ($scope, phones) {
  $scope.phones = phones;
  $scope.orderProp = 'age';
}]);

Since this code is derived from the question/most popular answer it is untested, but it should send you in the right direction if you already understand how to make minification friendly angular code. The one part that my own code didn't requires was an injection of "Phone" into the resolve function for 'phones', nor did I use any 'delay' object at all.

I also recommend this youtube video http://www.youtube.com/watch?v=P6KITGRQujQ&list=UUKW92i7iQFuNILqQOUOCrFw&index=4&feature=plcp , which helped me quite a bit

Should it interest you I've decided to also paste my own code (Written in coffeescript) so you can see how I got it working.

FYI, in advance I use a generic controller that helps me do CRUD on several models:

appModule.config ['$routeProvider', ($routeProvider) ->
  genericControllers = ["boards","teachers","classrooms","students"]
  for controllerName in genericControllers
    $routeProvider
      .when "/#{controllerName}/",
        action: 'confirmLogin'
        controller: 'GenericController'
        controllerName: controllerName
        templateUrl: "/static/templates/#{controllerName}.html"
        resolve:
          items : ["$q", "$route", "$http", ($q, $route, $http) ->
             deferred = $q.defer()
             controllerName = $route.current.controllerName
             $http(
               method: "GET"
               url: "/api/#{controllerName}/"
             )
             .success (response) ->
               deferred.resolve(response.payload)
             .error (response) ->
               deferred.reject(response.message)

             return deferred.promise
          ]

  $routeProvider
    .otherwise
      redirectTo: '/'
      action: 'checkStatus'
]

appModule.controller "GenericController", ["$scope", "$route", "$http", "$cookies", "items", ($scope, $route, $http, $cookies, items) ->

  $scope.items = items
      #etc ....
    ]
bitwit
  • 2,589
  • 2
  • 28
  • 33
  • Do I infer correctly from your example and my failed attempts that it's now impossible to reference a `resolve` function in the controller, in recent versions of Angular? So it has to be declared right in the config as it is here? – XML Aug 06 '13 at 23:26
  • @XMLilley I'm pretty sure that's the case. This example was from 1.1.2 when I wrote it, I believe. I did not see any documentation on putting resolve inside of a controller – bitwit Aug 07 '13 at 14:25
  • 2
    Cool, thanks. There's lots of examples of doing so on SO (like the top two here), but they're all from 2012 and early 2013. It's an elegant approach, but appears to be deprecated. The cleanest alternative now seems to be writing individual services that are promise objects. – XML Aug 07 '13 at 15:04
  • Thanks this worked for me. For anyone else who gets errors about undefined `$defer` service, do note that in version 1.5.7 of AngularJS, you want to use `$timeout` instead. – racl101 Jul 22 '16 at 20:08
18

This commit, which is part of version 1.1.5 and above, exposes the $promise object of $resource. Versions of ngResource including this commit allow resolving resources like this:

$routeProvider

resolve: {
    data: function(Resource) {
        return Resource.get().$promise;
    }
}

controller

app.controller('ResourceCtrl', ['$scope', 'data', function($scope, data) {

    $scope.data = data;

}]);
Max Hoffmann
  • 2,957
  • 1
  • 17
  • 13
  • Which versions include that commit, please? – XML Aug 06 '13 at 23:13
  • The latest unstable version (1.1.5) includes this commit. https://ajax.googleapis.com/ajax/libs/angularjs/1.1.5/angular.min.js – Max Hoffmann Aug 10 '13 at 22:56
  • I like this less verbose approach. Would be nice to create a promise from the actual data object and pass that in directly, but this is so little code that it works nicely. – Sam Barnum Aug 23 '13 at 14:55
  • 1
    How would the resource access $routeParams? For example: in `GET '/api/1/apps/:appId'` --> `App.get({id: $routeParams.appId}).$promise();` I can't use like this – zeronone Feb 13 '14 at 01:36
  • 2
    @zeronone you inject `$route` to your resolve and use `$route.current.params`. Be careful, `$routeParams` is still pointing to the old route. – Brice Stacey Apr 26 '14 at 13:50
16

This snippet is dependency injection friendly (I even use it in combination of ngmin and uglify) and it's a more elegant domain driven based solution.

The example below registers a Phone resource and a constant phoneRoutes, which contains all your routing information for that (phone) domain. Something I didn't like in the provided answer was the location of the resolve logic -- the main module should not know anything or be bothered about the way the resource arguments are provided to the controller. This way the logic stays in the same domain.

Note: if you're using ngmin (and if you're not: you should) you only have to write the resolve functions with the DI array convention.

angular.module('myApp').factory('Phone',function ($resource) {
  return $resource('/api/phone/:id', {id: '@id'});
}).constant('phoneRoutes', {
    '/phone': {
      templateUrl: 'app/phone/index.tmpl.html',
      controller: 'PhoneIndexController'
    },
    '/phone/create': {
      templateUrl: 'app/phone/edit.tmpl.html',
      controller: 'PhoneEditController',
      resolve: {
        phone: ['$route', 'Phone', function ($route, Phone) {
          return new Phone();
        }]
      }
    },
    '/phone/edit/:id': {
      templateUrl: 'app/phone/edit.tmpl.html',
      controller: 'PhoneEditController',
      resolve: {
        form: ['$route', 'Phone', function ($route, Phone) {
          return Phone.get({ id: $route.current.params.id }).$promise;
        }]
      }
    }
  });

The next piece is injecting the routing data when the module is in the configure state and applying it to the $routeProvider.

angular.module('myApp').config(function ($routeProvider, 
                                         phoneRoutes, 
                                         /* ... otherRoutes ... */) {

  $routeProvider.when('/', { templateUrl: 'app/main/index.tmpl.html' });

  // Loop through all paths provided by the injected route data.

  angular.forEach(phoneRoutes, function(routeData, path) {
    $routeProvider.when(path, routeData);
  });

  $routeProvider.otherwise({ redirectTo: '/' });

});

Testing the route configuration with this setup is also pretty easy:

describe('phoneRoutes', function() {

  it('should match route configuration', function() {

    module('myApp');

    // Mock the Phone resource
    function PhoneMock() {}
    PhoneMock.get = function() { return {}; };

    module(function($provide) {
      $provide.value('Phone', FormMock);
    });

    inject(function($route, $location, $rootScope, phoneRoutes) {
      angular.forEach(phoneRoutes, function (routeData, path) {

        $location.path(path);
        $rootScope.$digest();

        expect($route.current.templateUrl).toBe(routeData.templateUrl);
        expect($route.current.controller).toBe(routeData.controller);
      });
    });
  });
});

You can see it in full glory in my latest (upcoming) experiment. Although this method works fine for me, I really wonder why the $injector isn't delaying construction of anything when it detects injection of anything that is a promise object; it would make things soooOOOOOooOOOOO much easier.

Edit: used Angular v1.2(rc2)

null
  • 7,906
  • 3
  • 36
  • 37
  • 2
    This excellent answer seems much more in line with the "Angular" philosophy (encapsulation, etc). We should all be making a conscious effort to stop logic from creeping all over the codebase like kudzu. – zakdances Oct 22 '13 at 23:06
  • `I really wonder why the $injector isn't delaying construction of anything when it detects injection of anything that is a promise object` I'm guessing they omitted this functionality because it might encourages design patterns that negatively effect the responsiveness of apps. The ideal app in their mind is one that's truly asynchronous, so resolving should be an edge case. – zakdances Oct 22 '13 at 23:09
11

Delaying showing the route is sure to lead to an asynchronous tangle... why not simply track the loading status of your main entity and use that in the view. For example in your controller you might use both the success and error callbacks on ngResource:

$scope.httpStatus = 0; // in progress
$scope.projects = $resource.query('/projects', function() {
    $scope.httpStatus = 200;
  }, function(response) {
    $scope.httpStatus = response.status;
  });

Then in the view you could do whatever:

<div ng-show="httpStatus == 0">
    Loading
</div>
<div ng-show="httpStatus == 200">
    Real stuff
    <div ng-repeat="project in projects">
         ...
    </div>
</div>
<div ng-show="httpStatus >= 400">
    Error, not found, etc. Could distinguish 4xx not found from 
    5xx server error even.
</div>
jpsimons
  • 27,382
  • 3
  • 35
  • 45
  • 6
    Perhaps exposing HTTP status to the view isn't right, anymore than dealing with CSS classes and DOM elements belong in the controller. I'd probably use the same idea but abstract status away in isValid() and isLoaded(). – jpsimons Sep 08 '12 at 02:03
  • 1
    This is indeed not the best separation of concerns, plus that it will crash if you have nested controllers that depend on the specific object. – null Oct 06 '13 at 21:07
  • This is pretty clever.. as to exposing the status codes to the view, you could just stick the http logic in scope properties within the controller, then bind to them. Also if you making multiple ajax calls that happen in the background you will want to do this anyway. – KingOfHypocrites Jul 21 '15 at 13:00
  • This would be fine if the issue was a matter of simply delaying a view. But resolve is best used if you need to delay the *instantiation* of a controller-- not just the view. (Ex: If you need to be sure your JSON is loaded because your controller passes it to a directive before it's wired.) From the docs: "the router will wait for them all to be resolved or one to be rejected *before the controller is instantiated*". – Dan May 17 '17 at 22:57
8

I worked from Misko's code above and this is what I've done with it. This is a more current solution since $defer has been changed to $timeout. Substituting $timeout however will wait for the timeout period (in Misko's code, 1 second), then return the data hoping it's resolved in time. With this way, it returns asap.

function PhoneListCtrl($scope, phones) {
  $scope.phones = phones;
  $scope.orderProp = 'age';
}

PhoneListCtrl.resolve = {

  phones: function($q, Phone) {
    var deferred = $q.defer();

    Phone.query(function(phones) {
        deferred.resolve(phones);
    });

    return deferred.promise;
  }
}
F Lekschas
  • 12,481
  • 10
  • 60
  • 72
Justen
  • 4,859
  • 9
  • 44
  • 68
7

Using AngularJS 1.1.5

Updating the 'phones' function in Justen's answer using AngularJS 1.1.5 syntax.

Original:

phones: function($q, Phone) {
    var deferred = $q.defer();

    Phone.query(function(phones) {
        deferred.resolve(phones);
    });

    return deferred.promise;
}

Updated:

phones: function(Phone) {
    return Phone.query().$promise;
}

Much shorter thanks to the Angular team and contributors. :)

This is also the answer of Maximilian Hoffmann. Apparently that commit made it into 1.1.5.

OJ Raqueño
  • 4,471
  • 2
  • 17
  • 30
5

You can use $routeProvider resolve property to delay route change until data is loaded.

angular.module('app', ['ngRoute']).
  config(['$routeProvider', function($routeProvider, EntitiesCtrlResolve, EntityCtrlResolve) {
    $routeProvider.
      when('/entities', {
        templateUrl: 'entities.html', 
        controller: 'EntitiesCtrl', 
        resolve: EntitiesCtrlResolve
      }).
      when('/entity/:entityId', {
        templateUrl: 'entity.html', 
        controller: 'EntityCtrl', 
        resolve: EntityCtrlResolve
      }).
      otherwise({redirectTo: '/entities'});
}]);

Notice that the resolve property is defined on route.

EntitiesCtrlResolve and EntityCtrlResolve is constant objects defined in same file as EntitiesCtrl and EntityCtrl controllers.

// EntitiesCtrl.js

angular.module('app').constant('EntitiesCtrlResolve', {
  Entities: function(EntitiesService) {
    return EntitiesService.getAll();
  }
});

angular.module('app').controller('EntitiesCtrl', function(Entities) {
  $scope.entities = Entities;

  // some code..
});

// EntityCtrl.js

angular.module('app').constant('EntityCtrlResolve', {
  Entity: function($route, EntitiesService) {
    return EntitiesService.getById($route.current.params.projectId);
  }
});

angular.module('app').controller('EntityCtrl', function(Entity) {
  $scope.entity = Entity;

  // some code..
});
Bohdan Lyzanets
  • 1,652
  • 22
  • 25
3

I like darkporter's idea because it will be easy for a dev team new to AngularJS to understand and worked straight away.

I created this adaptation which uses 2 divs, one for loader bar and another for actual content displayed after data is loaded. Error handling would be done elsewhere.

Add a 'ready' flag to $scope:

$http({method: 'GET', url: '...'}).
    success(function(data, status, headers, config) {
        $scope.dataForView = data;      
        $scope.ready = true;  // <-- set true after loaded
    })
});

In html view:

<div ng-show="!ready">

    <!-- Show loading graphic, e.g. Twitter Boostrap progress bar -->
    <div class="progress progress-striped active">
        <div class="bar" style="width: 100%;"></div>
    </div>

</div>

<div ng-show="ready">

    <!-- Real content goes here and will appear after loading -->

</div>

See also: Boostrap progress bar docs

reggoodwin
  • 1,514
  • 13
  • 14
  • Falls apart a bit if you're loading multiple pieces of data. How do you know if everything loaded? – toxaq Oct 13 '13 at 23:32
  • Things have moved on since I added this answer in Feb, with a lot more activity on this page. Looks like there is better support in Angular for solving this problem now than what is suggested here. Cheers, – reggoodwin Oct 16 '13 at 08:08
  • I arrive a bit late, but dealing with multiple pieces of data is not of big concern. You just have to use separate variables (booleans : isReadyData1, isReadyData2 etc.) for each request, and set the $scope.ready = isReadyData1 && isReadyData2 ...; works well for me. – GuillaumeA Nov 16 '13 at 20:34
1

I liked above answers and learned a lot from them but there is something that is missing in most of the above answers.

I was stuck in a similar scenario where I was resolving url with some data that is fetched in the first request from the server. Problem I faced was what if the promise is rejected.

I was using a custom provider which used to return a Promise which was resolved by the resolve of $routeProvider at the time of config phase.

What I want to stress here is the concept of when it does something like this.

It sees the url in url bar and then respective when block in called controller and view is referred so far so good.

Lets say I have following config phase code.

App.when('/', {
   templateUrl: '/assets/campaigns/index.html',
   controller: 'CampaignListCtr',
   resolve : {
      Auth : function(){
         return AuthServiceProvider.auth('campaign');
      }
   }
})
// Default route
.otherwise({
   redirectTo: '/segments'
});

On root url in browser first block of run get called otherwise otherwise gets called.

Let's imagine a scenario I hit rootUrl in address bar AuthServicePrivider.auth() function gets called.

Lets say Promise returned is in reject state what then???

Nothing gets rendered at all.

Otherwise block will not get executed as it is for any url which is not defined in the config block and is unknown to angularJs config phase.

We will have to handle the event that gets fired when this promise is not resolved. On failure $routeChangeErorr gets fired on $rootScope.

It can be captured as shown in code below.

$rootScope.$on('$routeChangeError', function(event, current, previous, rejection){
    // Use params in redirection logic.
    // event is the routeChangeEvent
    // current is the current url
    // previous is the previous url
    $location.path($rootScope.rootPath);
});

IMO It's generally a good idea to put event tracking code in run block of application. This code run just after the config phase of the application.

App.run(['$routeParams', '$rootScope', '$location', function($routeParams, $rootScope, $location){
   $rootScope.rootPath = "my custom path";
   // Event to listen to all the routeChangeErrors raised
   // by the resolve in config part of application
   $rootScope.$on('$routeChangeError', function(event, current, previous, rejection){
       // I am redirecting to rootPath I have set above.
       $location.path($rootScope.rootPath);
   });
}]);

This way we can handle promise failure at the time of config phase.

Ashish Singh
  • 739
  • 3
  • 8
  • 21
0

I have had a complex multi-level sliding panel interface, with disabled screen layer. Creating directive on disable screen layer that would create click event to execute the state like

$state.go('account.stream.social.view');

were producing a flicking effect. history.back() instead of it worked ok, however its not always back in history in my case. SO what I find out is that if I simply create attribute href on my disable screen instead of state.go , worked like a charm.

<a class="disable-screen" back></a>

Directive 'back'

app.directive('back', [ '$rootScope', function($rootScope) {

    return {
        restrict : 'A',
        link : function(scope, element, attrs) {
            element.attr('href', $rootScope.previousState.replace(/\./gi, '/'));
        }
    };

} ]);

app.js I just save previous state

app.run(function($rootScope, $state) {      

    $rootScope.$on("$stateChangeStart", function(event, toState, toParams, fromState, fromParams) {         

        $rootScope.previousState = fromState.name;
        $rootScope.currentState = toState.name;


    });
});
Dima
  • 1,045
  • 14
  • 23
-2

One possible solution might be to use the ng-cloak directive with the element where we are using the models e.g.

<div ng-cloak="">
  Value in  myModel is: {{myModel}}
</div>

I think this one takes least effort.

Devendra Singh
  • 81
  • 1
  • 2
  • 9