Skip to content

$stateChangeStart -> event.preventDefault() forces wrong url change. #1699

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
sebastian-zarzycki opened this issue Jan 21, 2015 · 29 comments
Closed

Comments

@sebastian-zarzycki
Copy link

in my main controller:

$rootScope.$on('$locationChangeStart', function(event, toUrl, fromUrl) {
  console.log(toUrl);
});

$rootScope.$on('$stateChangeStart', function (event, toState, toParams, fromState, fromParams) {
    if (notLoggedIn) {
        event.preventDefault();
        $state.go('login');
    }
});

states definition (just the relevant portion, other states are obviously there):

$urlRouterProvider.when('', '/home');
$urlRouterProvider.otherwise('/error/not-found');

console output

http://localhost/app/
http://localhost/app/#!/home
http://localhost/app/#!/
http://localhost/app/#!/error/not-found

so it first loads the application
http://localhost/app/
then it applies the first "when" condition on urlRouter level, so it goes to /home.
http://localhost/app/#!/home
Then, the $stateChangeStart triggers, checks that user is not logged is (own logic), and does event.preventDefault(). At this stage, instantly, the url is reset
http://localhost/app/#!/
and somehow, the "otherwise" condition triggers in
http://localhost/app/#!/error/not-found

This 3rd step is unexpected and breaks the flow of the application (preventing interferes with urlRouter)

@christopherthielen
Copy link
Contributor

I agree the behavior is odd, but what state do you expect the user to end up in?

By the way, usually people do a $state.go('login') after the preventDefault

@sebastian-zarzycki
Copy link
Author

Yes, I do have redirect to login in my code - I just cut it out here, for brevity.

As for the state, I think it's pretty obvious - this is a $stateChangeStart, right? If it's prevented, then user remains in the state he started from. Simply put, the state don't change. I see no reason, why preventing state results in changing the url.

@christopherthielen
Copy link
Contributor

Hi sebastian,
It's not obvious from the details you have provided. If I understand correctly:

  • User navigates to your app
  • Initial transition starts (no state active yet)
  • .when("", "/home") invoked (no state loaded yet)
  • the transition to 'home' state is cancelled.
  • What state should I be in? There is no previous state.

However, if you do have a redirect in your code, then this behavior is odd, and I'd need more details. Can you make a plunk?

@sebastian-zarzycki
Copy link
Author

Yes, that's it. I have the login redirect right after the preventDefault (edited my initial code), pretty much the same as you've described. But it will never reach it, as preventDefault resets the url and then triggers the "otherwise" condition on urlRouter. I do understand that in this particular case, there's no previous state and maybe the url reset makes sense. But it collides with otherwise, which prevents the redirect from working.

I'm not very good at providing sample codes on these external services, sorry. This should do, though. angular 1.3.8, newest ui-router.

@christopherthielen
Copy link
Contributor

http://bit.ly/UIR-Plunk

Put your code in a plunk and reproduce it, then I'll take a look

@michaelcox
Copy link

I happen to have come across the same issue. Here's a fork of your plunk, with just a couple lines added:
http://plnkr.co/edit/rU4IXC

I think this captures what @sebastian-zarzycki was trying to accomplish. We want the default route to be "home", unless for some reason it doesn't pass some filter. So we do a check (line 29) and then redirect with a $state.go. But as he says, the evt.preventDefault() will not just stop the transition, but actually load the "otherwise" route. If you open the console you'll see /home is hit when we didn't want it to be.

I think the confusion is that the syntax evt.preventDefault() elsewhere in javascript usually has the effect of stopping an event cold. In this case it stops the event but then goes a stop further and loads a different route that you may not want to actually load. I think it would make more sense if stopping a route change this way actually stopped the route change and left it to the developer to provide a valid next step, which would likely be a $state.go() to another location.

As an aside, in the meantime, it seems the workaround would be to have the "otherwise" route be a dummy placeholder, and always check state transitions to make sure the user goes somewhere correct. That way even if "otherwise" gets loaded unintentionally, it has no functionality associated with it.

@sebastian-zarzycki
Copy link
Author

Pretty much, thanks for this, Michael.

@gitnik
Copy link

gitnik commented Mar 16, 2015

Same issue here. Does anyone know of a workaround?

@adammartin1981
Copy link

The work around I found was:-

$rootScope.$on('$stateChangeStart', function (event, toState, toParams, fromState, fromParams) {
    if (notLoggedIn) {
        event.preventDefault();
        return $state.go('login');
    }

    return;
});

The key for me was to return the new state

@radotzki
Copy link

radotzki commented Jun 2, 2015

Thanks @adammartin1981, works for me.

@fgarit
Copy link

fgarit commented Jun 12, 2015

@adammartin1981 looking at the source, I could be wrong, but I don't think the return value of an event is checked.

angular-ui-router.js version 0.2.15 line 3229

if ($rootScope.$broadcast('$stateChangeStart', to.self, toParams, from.self, fromParams).defaultPrevented) {

All it does is check if the defaultPrevented function was called on the event.

@fgarit
Copy link

fgarit commented Jun 12, 2015

My case is a bit more complicated. The browser back button is broken (it loops).

The following does not break the history API:

$rootScope.$on('$stateChangeStart', function (event, toState, toParams, fromState, fromParams) {
    if (notLoggedIn) {
        event.preventDefault();
        $state.go('login');
    }
});

The following does:

$rootScope.$on('$stateChangeStart', function (event, toState, toParams, fromState, fromParams) {
    event.preventDefault();

    $timeout(function() {
        if (notLoggedIn) {
            $state.go('login');
        } else {
            $state.go(toState, toParams);
        }
    });
});

(obviously, I'm not just setting a timeout for the heck of it, but I'm doing some ajax call)

My fix is the following, in angular-ui-router.js version 0.2.15 line 3231:

if ($rootScope.$broadcast('$stateChangeStart', to.self, toParams, from.self, fromParams).defaultPrevented) {
  $rootScope.$broadcast('$stateChangeCancel', to.self, toParams, from.self, fromParams);
  //$urlRouter.update(); // this line causes the issue in case of browser nav
  return TransitionPrevented;
}

The problematic case with this $urlRouter.update call is the following:
When I hit the browser back button, the url is updated by the browser before $stateChangeStart is called (as opposed to when there is a normal $state.go nav).

Since event.preventDefault was called on the $stateChangeStart event, $urlRouter.update is called. In the code below, $location.url() is not equal to location ($location.url() is the one of the toState and location is the one of the fromState).
So $location.url(location); $location.replace(); are called with the url of the fromState which creates a new browser history entry I think (this explains the loop). I'm using chrome 43.0 btw, but I don't think it's relevant.

angular-ui-router version 0.2.15 line 2064:

update: function(read) {
  if (read) {
    location = $location.url();
    return;
  }
  if ($location.url() === location) return; // toStateUrl !== from fromStateUrl

  $location.url(location); // $location.url(fromStateUrl);
  $location.replace();
},

I suppose there is a good reason for that $urlRouter.update call. Maybe we could set some property on the $stateChangeStart event to let the router know not to make that call.

var event = $rootScope.$broadcast('$stateChangeStart', to.self, toParams, from.self, fromParams);
if (event.defaultPrevented) {
  $rootScope.$broadcast('$stateChangeCancel', to.self, toParams, from.self, fromParams);
  if (event.noUpdate !== true) {
    $urlRouter.update();
  }
  return TransitionPrevented;
}
event.preventDefault();
event.noUpdate = true;

It may not look very clean, but I can't think of another way around.

@eddiemonge
Copy link
Contributor

closing this since it seems like a workaround for the original problem is there. @FLO-G if you are having a separate problem (seems like it?) then please open a new issue for that.

@readme42
Copy link

I'm sorry, but #1699 (comment) does not work for me. event.preventDefault() still executes a state change to the otherwise route and this create an infinite loop in my app. Anyone else with this problem?

I fully agree with #1699 (comment), in my opinion a preventDefault()-Call should prevent the event from happen - and nothing else. If i want a following state change, i will do it by myself.

@readme42
Copy link

@michaelcox i tried the "dummyState"-approach you mentioned here #1699 (comment).

$urlRouterProvider.otherwise("/dummyState");

.state("dummyState", {
    url: "/dummyState"
})

But what then? If i listen to $stateChangeStart, the problem is the same and i would need preventDefault() which would result in an infinite loop. If i listen to $stateChangeSuccess, my browser history is broken up with dummyState entries. Did i get you wrong?

@readme42
Copy link

After some issue studying, rewriting my otherwise statement to this

$urlRouterProvider.otherwise(function($injector) {
    var $state = $injector.get("$state");
    $state.go("error.404");
});

solved my preventDefault() issue.

(Sorry, don't know where i copied that from)

@hakib
Copy link

hakib commented Nov 29, 2015

Same issue here.

Got stuck in an infinite loop when state transition from otherwise was not reflected in $stateChangeStart to argument. Returning the state as suggested by @adammartin1981 did not work for me but using $state as @readme42 suggested did the trick.

Thanks.

@Misiu
Copy link

Misiu commented Jan 20, 2016

@readme42 thanks :) I had issue with Ionic app, Your code fixed it!

@jaitor1
Copy link

jaitor1 commented Feb 14, 2016

@readme42 thanks :) Your solution worked for me too.

@jhondev
Copy link

jhondev commented Feb 18, 2016

@readme42 thanks :)

@Misiu
Copy link

Misiu commented Feb 18, 2016

@eddiemonge Can't this be fixed in source?

//default - this isn't working
$urlRouterProvider.otherwise('/login');
//this helped!
$urlRouterProvider.otherwise(function ($injector) {
    var $state = $injector.get("$state");
    $state.go("login");
});

@AdamGerthel
Copy link

#1699 (comment) solved it for me

@DanielCaspers
Copy link

@hakib Could you describe the issue you had with the infinite looping in more detail? I have a feeling I am running into something similar... Perhaps post code snippets of before and after please?

@hakib
Copy link

hakib commented Mar 5, 2016

@DanielCaspers I had an infinite loop because I was also intercepting $stateChangeStart and redirecting based on some conditions (like if the user is authenticated). What I found was that without the solution suggested by @readme42, the to state was not being update so my interceptor kept redirecting without actually changing the route (and so the infinite loop).

@DanielCaspers
Copy link

Thanks @hakib. This inspired me in finding my problem, although mine wasn't related to this. In the situation I was working on, I believe it was a logic error because top.location = location.pathname; was bouncing the app back to start. I realize this is a rather specific mistake but hopefully it will help someone else! :)

@omma2289
Copy link

@hakib I had the same issue and can confirm that the code provided by @readme42 fixes the problem.

@Buddikazz
Copy link

Buddikazz commented Jan 9, 2017

.state('login', {
      url: '/login',
      templateUrl: 'templates/login.html',
      controller: 'LoginCtrl',
      onEnter: function($state, Auth){
      if(Auth.isAuth()){
      $state.go('tab.neworder');//place u want to go when login
      }
      }
  })

use this this work perfectly

@adrianzielonka
Copy link

Was experiencing this issue myself with an outdated version of ui-router v0.2.15 and tried all the work arounds to no avail. For anyone experiencing this issue, ui-router v0.4.2 has now rectified the issue.

@ravimallya
Copy link

ravimallya commented May 19, 2018

@readme42 I also had the same issue in the latest version of Ionic v1. I was receiving '$rootScope:infdig] 10 $digest() iterations reached. Aborting!' error.
What I observed is, the error won't come if we access the url like: http://localhost:8100/#/home directly. However, if you access the domain and port, i.e. http://localhost:8100 it will throw error.

I am using this code to check:
if (angular.isUndefined($localStorage.UserId) || $localStorage.UserId === null) { var requireLogin = toState.data.requireLogin; console.log(requireLogin); if (requireLogin) { event.preventDefault(); $state.go('login'); } }

The following code fixed it.
$urlRouterProvider.otherwise(function($injector) { var $state = $injector.get("$state"); $state.go("home"); });

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests