88

How do I cancel route change event in AngularJs?

My current code is

$rootScope.$on("$routeChangeStart", function (event, next, current) {

// do some validation checks
if(validation checks fails){

    console.log("validation failed");

    window.history.back(); // Cancel Route Change and stay on current page  

}
});

with this even if the validation fails Angular pulls the next template and associated data and then immediately switches back to previous view/route. I don't want angular to pull next template & data if validation fails, ideally there should be no window.history.back(). I even tried event.preventDefault() but no use.

R Arun
  • 1,165
  • 2
  • 10
  • 13

10 Answers10

185

Instead of $routeChangeStart use $locationChangeStart

Here's the discussion about it from the angularjs guys: https://github.com/angular/angular.js/issues/2109

Edit 3/6/2018 You can find it in the docs: https://docs.angularjs.org/api/ng/service/$location#event-$locationChangeStart

Example:

$scope.$on('$locationChangeStart', function(event, next, current) {
    if ($scope.form.$invalid) {
       event.preventDefault();
    }
});
Mathew Berg
  • 28,625
  • 11
  • 69
  • 90
  • 8
    The problem with this is that there is no way to access the route parameters collection. If you are trying to validate route parameters this solution is no good. – KingOfHypocrites Aug 30 '14 at 02:22
  • 1
    Please note, when using `$routeChangeStart` the `next` variable is just a string and it can't contain any data (for example you can't access to defined earlier `authorizedRoles` variable) – WelcomeTo Nov 04 '14 at 11:45
  • @KingOfHypocrites you can't get route parameters, but you can get `$location.path()` and `$location.search()` – Design by Adrian Apr 06 '15 at 15:59
  • 2
    Can you/is it advisable to do this on the rootScope if you want to track all route changes? Or is there a more palatable alternative? – maxm May 31 '15 at 20:11
38

A more complete code sample, using $locationChangeStart

// assuming you have a module called app, with a 
angular.module('app')
  .controller(
    'MyRootController',
    function($scope, $location, $rootScope, $log) {
      // your controller initialization here ...
      $rootScope.$on("$locationChangeStart", function(event, next, current) { 
        $log.info("location changing to:" + next); 
      });
    }
  );

I'm not completely happy with hooking this up in my root controller (top level controller). If there is a better pattern, I'd love to know. I'm new to angular :-)

Shyam Habarakada
  • 15,367
  • 3
  • 36
  • 47
12

A solution is to broadcast a 'notAuthorized' event, and catch it in the main scope to re-change the location. I think it is not the best solution, but it worked for me:

myApp.run(['$rootScope', 'LoginService',
    function ($rootScope, LoginService) {
        $rootScope.$on('$routeChangeStart', function (event, next, current) {
            var authorizedRoles = next.data ? next.data.authorizedRoles : null;
            if (LoginService.isAuthenticated()) {
                if (!LoginService.isAuthorized(authorizedRoles)) {
                    $rootScope.$broadcast('notAuthorized');
                }
            }
        });
    }
]);

and in my Main Controller:

    $scope.$on('notAuthorized', function(){
        $location.path('/forbidden');
    });

Note: there is some discussion about this problem on angular site, not yet solved: https://github.com/angular/angular.js/pull/4192

EDIT:

To answer the comment, here is more information about the LoginService works. It contains 3 functions:

  1. login() (name is misleading) do a request to the server to get information about the (previously) logged user. There is another login page which just populate the current user state in the server (using SpringSecurity framework). My Web Services are not truely stateless, but I preferred to let that famous framework handle my security .
  2. isAuthenticated() just search if the client Session is filled with data, which means it has been authenticated before (*)
  3. isAuthorized() handled access rights (out of scope for this topic).

(*) My Session is populated when the route change. I have overridden then when() method to populate the session when empty.

Here is the code :

services.factory('LoginService', ['$http', 'Session', '$q',
function($http, Session, $q){
    return {
        login: function () {
            var defer = $q.defer();
            $http({method: 'GET', url: restBaseUrl + '/currentUser'})
                .success(function (data) {
                    defer.resolve(data);
                });
            return defer.promise;
        },
        isAuthenticated: function () {
            return !!Session.userLogin;
        },
        isAuthorized: function (authorizedRoles) {
            if (!angular.isArray(authorizedRoles)) {
                authorizedRoles = [authorizedRoles];
            }

            return (this.isAuthenticated() &&  authorizedRoles.indexOf(Session.userRole) !== -1);
        }
    };
}]);

myApp.service('Session', ['$rootScope',
    this.create = function (userId,userLogin, userRole, userMail, userName, userLastName, userLanguage) {
        //User info
        this.userId = userId;
        this.userLogin = userLogin;
        this.userRole = userRole;
        this.userMail = userMail;
        this.userName = userName;
        this.userLastName = userLastName;
        this.userLanguage = userLanguage;
    };

    this.destroy = function () {
        this.userId = null;
        this.userLogin = null;
        this.userRole = null;
        this.userMail = null;
        this.userName = null;
        this.userLastName = null;
        this.userLanguage = null;
        sessionStorage.clear();
    };

    return this;
}]);

myApp.config(['$routeProvider', 'USER_ROLES', function ($routeProvider, USER_ROLES) {
    $routeProvider.accessWhen = function (path, route) {
        if (route.resolve == null) {
            route.resolve = {
                user: ['LoginService','Session',function (LoginService, Session) {
                    if (!LoginService.isAuthenticated())
                        return LoginService.login().then(function (data) {
                            Session.create(data.id, data.login, data.role, data.email, data.firstName, data.lastName, data.language);
                            return data;
                        });
                }]
            }
        } else {
            for (key in route.resolve) {
                var func = route.resolve[key];
                route.resolve[key] = ['LoginService','Session','$injector',function (LoginService, Session, $injector) {
                    if (!LoginService.isAuthenticated())
                        return LoginService.login().then(function (data) {
                            Session.create(data.id, data.login, data.role, data.email, data.firstName, data.lastName, data.language);
                            return func(Session, $injector);
                        });
                    else
                        return func(Session, $injector);
                }];
            }
        }
    return $routeProvider.when(path, route);
    };

    //use accessWhen instead of when
    $routeProvider.
        accessWhen('/home', {
            templateUrl: 'partials/dashboard.html',
            controller: 'DashboardCtrl',
            data: {authorizedRoles: [USER_ROLES.superAdmin, USER_ROLES.admin, USER_ROLES.system, USER_ROLES.user]},
            resolve: {nextEvents: function (Session, $injector) {
                $http = $injector.get('$http');
                return $http.get(actionBaseUrl + '/devices/nextEvents', {
                    params: {
                        userId: Session.userId, batch: {rows: 5, page: 1}
                    },
                    isArray: true}).then(function success(response) {
                    return response.data;
                });
            }
        }
    })
    ...
    .otherwise({
        redirectTo: '/home'
    });
}]);
Asterius
  • 2,180
  • 2
  • 19
  • 27
  • Can you please say what return `LoginService.isAuthenticated()` at first page loading? How you storing `currentUser`? What happends if user refreshes the page (user need again reenter credentials)? – WelcomeTo Nov 16 '14 at 20:12
  • I added more information about the LoginService in my original answer. The currentUser is provided by the server, and the route change handle any page refresh, there is no need for the user to log again. – Asterius Nov 17 '14 at 08:57
4

For anyone stumbling upon this is an old question, (at least in angular 1.4) you can do this:

 .run(function($rootScope, authenticationService) {
        $rootScope.$on('$routeChangeStart', function (event, next) {
            if (next.require == undefined) return

            var require = next.require
            var authorized = authenticationService.satisfy(require);

            if (!authorized) {
                $rootScope.error = "Not authorized!"
                event.preventDefault()
            }
        })
      })
Ákos Vandra-Meyer
  • 1,890
  • 1
  • 23
  • 40
1

This is my solution and it works for me but i don't know if i am on the right way cause i am new to web technologies.

var app = angular.module("app", ['ngRoute', 'ngCookies']);
app.run(function($rootScope, $location, $cookieStore){
$rootScope.$on('$routeChangeStart', function(event, route){
    if (route.mustBeLoggedOn && angular.isUndefined($cookieStore.get("user"))) {
        // reload the login route
        jError(
             'You must be logged on to visit this page',
             {
               autoHide : true,
               TimeShown : 3000,
               HorizontalPosition : 'right',
               VerticalPosition : 'top',
               onCompleted : function(){ 
               window.location = '#/signIn';
                 window.setTimeout(function(){

                 }, 3000)
             }
        });
    }
  });
});

app.config(function($routeProvider){
$routeProvider
    .when("/signIn",{
        controller: "SignInController",
        templateUrl: "partials/signIn.html",
        mustBeLoggedOn: false
});
1

i found this one relevant

var myApp = angular.module('myApp', []);

myApp.run(function($rootScope) {
    $rootScope.$on("$locationChangeStart", function(event, next, current) { 
        // handle route changes  
$rootScope.error = "Not authorized!"
                event.preventDefault()   
    });
});

my post may help some one in future.

Monojit Sarkar
  • 2,353
  • 8
  • 43
  • 94
1
var app=angular
    .module('myapp', [])
    .controller('myctrl', function($rootScope) {
        $rootScope.$on("locationChangeStart", function(event, next, current) {
        if (!confirm("location changing to:" + next)) { 
            event.preventDefault();
        }
    })
});
General Failure
  • 2,421
  • 4
  • 23
  • 49
  • 2
    While this code snippet may solve the question, [including an explanation](http://meta.stackexchange.com/questions/114762/explaining-entirely-code-based-answers) really helps to improve the quality of your post. Remember that you are answering the question for readers in the future, and those people might not know the reasons for your code suggestion. – dpr Sep 14 '17 at 07:45
0

In case you need to do stop the route from changing in the $routeChangeStart event (i.e. you want to perform some operation based on the next route), inject $route and inside $routeChangeStart call:

$route.reload()
mrt
  • 1,669
  • 3
  • 22
  • 32
  • 1
    I was hopeful, but in Angular 1.2.7 on Chrome this seems to cause a JS loop and the page freezes. – Nick Spacek Jan 17 '14 at 14:37
  • 1
    @NickSpacek The condition in which you call `$route.reload()` needs to be different or else it's running the same code again. This is the equivalent of creating a `while` loop with `true` as the condition. – Kevin Beal Oct 07 '14 at 17:31
0

Just to share, in my case I want to delay route resolution with $routeChangeStart. I've got a SomethingService that must load before the resolution of the route starts (yes, chatty application) hence I've got a promise to wait for. Maybe I found an hack... The resolution of the route goes in error if a resolve return a rejection. I broken the resolve configuration and I fix it back later.

    var rejectingResolve = {
        cancel: function ($q){
            // this will cancel $routeChangeStart
            return $q.reject();
        }
    }
    
    $rootScope.$on("$routeChangeStart", function(event, args, otherArgs) {
        var route = args.$$route,
            originalResolve = route.resolve;
    
        if ( ! SomethingService.isLoaded() ){

            SomethingService.load().then(function(){
                // fix previously destroyed route configuration
                route.resolve = originalResolve;
                
                $location.search("ts", new Date().getTime());
                // for redirections
                $location.replace();
            });

            // This doesn't work with $routeChangeStart: 
            // we need the following hack
            event.preventDefault();
            
            // This is an hack! 
            // We destroy route configuration, 
            // we fix it back when SomethingService.isLoaded
            route.resolve = rejectingResolve;
        } 
    });
Plap
  • 1,046
  • 1
  • 8
  • 14
0

I needed something to catch any navigation away from the page to see if any form data was changed, and prompt the user to either stay or leave and discard changes.

The only way it worked for me was to listen to the $locationChangeStart event, and that handler would first check for form changes, then preventDefault, then prompt to either cancel/stay (do nothing, the event is already canceled) or navigate by removing the event listener and manually navigating.

    vm.$onInit = () => {
        const cancelEventHandler = $scope.$on('$locationChangeStart', (event, newUrl) => {
            if (!angular.equals(vm.formData, vm.originalFormData)) {
                event.preventDefault();
                alertify.confirm('Closing will discard all changes. Are you sure?', yes => {
                    if (yes) {
                        cancelEventHandler();
                        $window.location.href = newUrl;
                    }
                });
            }
        });
    };

alertify.confirm takes a promise handler for the reply as its second argument.

Todd Hale
  • 480
  • 8
  • 15