20

My problem is actually very similar to the one found here:

AngularJs - cancel route change event

In short, I'm using $routeChangeStart and trying to change the current route using $location. When I do, the console shows me that the original page is still loads and is quickly overwritten by the new page.

The solution provided was to use $locationChangeStart instead of $routeChangeStart, which should work for preventing the extra redirect. Unfortunately, I'm using additional data in the $routeprovider that I need to access while changing the route (I use it to track page restrictions). Here's an example...

$routeProvider.
    when('/login', { controller: 'LoginCtrl', templateUrl: '/app/partial/login.html', access: false}).
    when('/home', { controller: 'HomeCtrl', templateUrl: '/app/partial/home.html', access: true}).
    otherwise({ redirectTo: '/login' });


$rootScope.$on('$routeChangeStart', function(event, next, current) {
    if(next.access){
        //Do Stuff
    }
    else{
        $location.path("/login");
        //This will load the current route first (ie: '/home'), and then
        //redirect the user to the correct 'login' route.
    }
});

With $routeChangeStart, I can use the "next" and "current" parameters (see AngularJS - $route) as objects to retrieve my 'access' values. With $locationChangeStart, those two parameters return url strings, not objects. So there seems to be no way to retrieve my 'access' values.

Is there any way I can combine the redirect-stopping power of $locationChangeStart with the object-flexibility of $routeChangeStart to achieve what I need?

Community
  • 1
  • 1
ThisLanham
  • 745
  • 3
  • 8
  • 20

4 Answers4

22

One approach that comes to mind is trying to use the resolve parameter for this:

var resolver = function(access) {
  return {
    load: function($q) {
      if (access) { // fire $routeChangeSuccess
        var deferred = $q.defer();
        deferred.resolve();
        return deferred.promise;
      } else { // fire $routeChangeError
        return $q.reject("/login");
      }
    }
  }
}

$routeProvider.
  when('/login', { controller: 'LoginCtrl', templateUrl: '/app/partial/login.html', resolve: resolver(false)}).
  when('/home', { controller: 'HomeCtrl', templateUrl: '/app/partial/home.html', resolve: resolver(true)}).
  otherwise({ redirectTo: '/login' });

Please note that I haven't tested the code above but I'm doing similar stuff in my projects.

Marius Soutier
  • 11,184
  • 1
  • 38
  • 48
  • Sorry for the late reply. When something works for me, I tend to get excited and move on. This ended up being a perfect solution for what I was doing. In the load function, I was able to add things like `$http` and `$location` to further process my requests. – ThisLanham Nov 05 '13 at 18:12
  • I had to make my load function look more like the one from Nick http://stackoverflow.com/a/19482187/1449799 (the reject wasn't working as written here) – Michael Jul 07 '14 at 12:25
  • 1
    `load` function should read more like this: `var deferred = $q.defer(); if (access) { deferred.resolve(); } else { deferred.reject(); $location.url('/login'); } return deferred.promise;` – Clay Jan 26 '15 at 23:35
  • @marius-soutier Very huge thanx for the `resolve` option of `when()` – andras.tim May 11 '15 at 17:29
14

I faced the same situation myself and my solution was aligned with what the OP intended to do.

I use the $locationChangeStart event and the $route service. By accessing $route.routes, I get a hold of all route objects defined with $routeProvider.

.run(function($rootScope, $route, $location) {
  $rootScope.$on('$locationChangeStart', function(ev, next, current) {
    // We need the path component of `next`. We can either process `next` and 
    // spit out its path component, or simply use $location.path(). I go with
    // the latter.
    var nextPath = $location.path();
    var nextRoute = $route.routes[nextPath]

    console.log(nextRoute.access); // There you go!
  });
})

To parse the path component out of an absolute URL:

var urlParsingNode = document.createElement('a');
urlParsingNode.href = next;  // say, next = 'http://www.abc.com/foo?name=joe
console.log(urlParsingNode.pathname)  // returns "/foo"
tamakisquare
  • 16,659
  • 26
  • 88
  • 129
  • what if there is query parameter in the url...how do u identify which router is being called – niran Dec 21 '13 at 06:44
  • @niran - I have updated my answer for you. Take a look. – tamakisquare Dec 22 '13 at 01:55
  • 3
    How does this work for routes with parameters? like /article/:articleId? – mkhatib Dec 28 '13 at 08:31
  • 1
    @mkhatib - That would be a little trickier. I notice that each object in `$route.routes` has a property named `regexp`. As its name suggested, it's a regular expression object that you can use to match against the path component you would get from parsing the full URL (mentioned above), while looping through `$route.routes`. I have never tried it but I think that should work. – tamakisquare Dec 29 '13 at 06:52
  • In order to match an arbitrary path containing a parameter against a parameterized route RegExp, you'd have to check against some subset of the `routes` objects, if not all. For example, you could narrow it down by the first component of the path, if it were never a parameter in any route. Still, iterating over objects to check against multiple RegExp seems inefficient, especially for a `$locationChangeStart` process. – nshew13 Jul 15 '14 at 20:21
7

Since version 1.3.0 you can actually use the newly introduced preventDefault-method. With that you can cancel the current route change and then apply your own custom redirect as shown in this github-issue:


$rootScope.$on("$routeChangeStart", function (event, next, current) {
    if (next.access) {
      event.preventDefault();
      $rootScope.$evalAsync(function() {
        $location.path('/login');
      });
    }
});

I implemented this method in my own project and it works perfectly. Hope it helps anyone else who stumbles across it.

snrlx
  • 4,987
  • 2
  • 27
  • 34
3

Nice answer Marius, put me on the right track. I'm doing something like this for access control. This works however...

  var resolver = function(route, routeEvent) {
  return {
    load: function($q) {
      deferred = $q.defer();
      if (routeEvent!=3) { // eventually will be a list of routeEvents that the logged-in user is not allowed to visit read from a db table configured by admin
        deferred = $q.defer();
        deferred.resolve();
        return deferred.promise;
      } else { // fire $routeChangeError
        alert("You don't have permissions to access this.");
        deferred.reject(route);
        return deferred.promise;
      }
    }
  }
}

  var jsonRoutes = [

    {'route' : '/logout', 'templateUrl': 'partials/login.html',   'controller' : 'LoginCtrl', 'routeEvent' : 1 },
    {'route' : '/orders', 'templateUrl': 'partials/orders.html',   'controller': 'OrderListCtrl', 'routeEvent' : 2 },
    {'route' : '/products', 'templateUrl': 'partials/products.html',   'controller': 'ProductListCtrl', 'routeEvent' : 3 },

...

];


// somewhere on successful login code add in the dynamic routes

angular.forEach(jsonRoutes, function(r) {
                $route.routes[r.route] = {templateUrl: r.templateUrl, controller: r.controller, routeEvent: r.routeEvent, resolve: resolver(r.route, r.routeEvent)};
                  });


// got some static routes too which don't have access control - user has to login right?

  config(['$routeProvider', function($routeProvider) {


  $routeProvider.
    when('/error',  {templateUrl: 'partials/error.html',   controller : ErrorCtrl,  routeEvent : 1 }).
    when('/login',  {templateUrl: 'partials/login.html',   controller : LoginCtrl, routeEvent : 1 }).
    when('/home',  {templateUrl: 'partials/home.html',   controller : HomeCtrl, routeEvent : 1 }).
    when('/logout', {templateUrl: 'partials/login.html',   controller : LoginCtrl, routeEvent : 1 }).
    otherwise( {redirectTo: '/error'} );   

When the /orders route is clicked the promise is rejected (with a pop-up, could be modal dialog) and the route isn't followed.

Hope this helps someone.

Philippe Boissonneault
  • 3,949
  • 3
  • 26
  • 33
Nick
  • 358
  • 2
  • 13