AngularJs - cancel route change event
Asked Answered
O

10

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.

Orelee answered 2/5, 2013 at 17:31 Comment(0)
M
186

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();
    }
});
Moo answered 2/5, 2013 at 19:42 Comment(4)
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.Assumption
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)Teressaterete
@Assumption you can't get route parameters, but you can get $location.path() and $location.search()Samantha
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?Stibine
C
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 :-)

Cheek answered 4/2, 2014 at 9:17 Comment(2)
This worked great for me, although I'm not trying to cancel my route change like the orig poster. Thanks!Hatbox
Yeah problem with rootScope is you have to remember to unbind that handler when your controller goes away.Dorcus
A
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'
    });
}]);
Arondell answered 1/9, 2014 at 19:15 Comment(2)
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)?Teressaterete
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.Arondell
S
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()
            }
        })
      })
Smoodge answered 16/10, 2015 at 7:36 Comment(2)
I wonder, do you get charged extra for the usage of braces or ";"?Gooseneck
@MatthiasJansen of course. And to top it all off, braces count double, and semicolons count triple.Italy
P
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
});
Phlebotomize answered 15/11, 2014 at 23:20 Comment(2)
How can you answer a question if you're not sure about your answer?Kreindler
I'm sure that it works. I'm not sure if this is the proper way. If you have a better solution i would like to see it.Phlebotomize
D
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.

Dispraise answered 10/5, 2017 at 20:32 Comment(0)
M
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();
        }
    })
});
Mammoth answered 14/9, 2017 at 7:27 Comment(1)
While this code snippet may solve the question, including an explanation 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.Peatroy
D
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()
Dabchick answered 11/10, 2013 at 16:8 Comment(2)
I was hopeful, but in Angular 1.2.7 on Chrome this seems to cause a JS loop and the page freezes.Cryoscope
@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.Takakotakakura
L
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;
        } 
    });
Langham answered 22/10, 2020 at 10:25 Comment(0)
S
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.

Sat answered 14/2, 2023 at 20:13 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.