Skip to content

$state.transitionTo('myStateName') inside "$stateChangeStart" handler looks like doing successful transition but visually not! #178

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
iomico opened this issue Jun 14, 2013 · 34 comments

Comments

@iomico
Copy link

iomico commented Jun 14, 2013

$state.transitionTo('myStateName') inside "$stateChangeStart" handler looks like doing successful transition but visually not!

After so-called successful transition current state shows expected value, but in reality my browser do not transitioned to it. Visually it's did't changed. What I doing wrong?

app.run([
        '$rootScope',
        '$state',
        '$location',
        '$stateParams',
        '$couchPotato',
        function(
            $rootScope,
            $state,
            $location,
            $stateParams,
            $couchPotato) {
                app.couchPotato = $couchPotato;
                $rootScope.$state = $state;
                $rootScope.$stateParams = $stateParams;

                // here is my event handler
                $rootScope.$on("$stateChangeStart", function (ev, to, toParams, from, fromParams) { 
                    if (/* my conditional expression */) {
                         $state.transitionTo('MyIntendedStateName');
                    }
               });
        }
]);
@laurelnaiad
Copy link

Try doing evt.preventDefault() before you call your intended transition.

Cool that you're using $couchPotato with ui-router! :)

@ksperling
Copy link
Contributor

Replacing the current transition inside the start event should probably work even without preventdefault, so I'll leave this open as a bug.

@iomico
Copy link
Author

iomico commented Jun 17, 2013

Replacing

$state.transitionTo('MyIntendedStateName');

with

$location.path('MyIntendedStateUrl');

works fine.

Even inserting evt.preventDefault() before $state.transitionTo('MyIntendedStateName') works fine.

evt.preventDefault();
$state.transitionTo('MyIntendedStateName');

(stu-salsbury, thank you for the workaround, and yes, couchPotato - awesome)

But using bare $state.transitionTo() function not working as expected. In other words - transition to expected StateName triggering without any issue, but in reality (visually) DOM did't redraw with new State content.
(ksperling, thank you for paying attention)

@biofractal
Copy link

I am having a similar problem but the symptoms are not so clearcut. Here are the examples I have tried and their effects (the code below code is CoffeeScript)

$rootScope.$on '$stateChangeStart', -> 
    $state.transitionTo 'login' if not securityService.isLoggedIn()

The code above has no effect. The state transition does not cause any redirect and the original, requested url is served regardless.

$rootScope.$on '$stateChangeStart', -> 
    $location.path '/login' if not securityService.isLoggedIn()

The code above has some effect. Note that clicking on any link whilst the user is not logged in should always redirect to login. This works fine for the first two or three clicks and the login page is served however subsequent clicks serve up the requested url even though the $location.path line is being correctly run.

$rootScope.$on '$stateChangeStart', (ev) ->
    if not securityService.isLoggedIn()
        ev.preventDefault()
        $location.path '/login'

The code above works as expected. Whenever the state changes, whilst the user is not logged in, then the login page is served correctly.

@nateabele
Copy link
Contributor

@biofractal Have you tried ev.preventDefault() with $state.transitionTo()? That was the correct fix that was mentioned above.

@biofractal
Copy link

@nateabele sorry I missed out the most important one :-)

if not securityService.isAuthenticated()
    ev.preventDefault()
    $state.transitionTo('login')

The code above does not work as expected. The browser is incorrectly redirected to the originally requested url, rather than /login url specified by the state transition, however the itemplate specified in by templeteUrl (for the requested url) is not rendered.

So the incorrect url is not redirected but the page remains blank.

@nateabele
Copy link
Contributor

@biofractal Okay then, you're welcome to create a Plunkr that reproduces the issue, and post the link here, per the instructions for reporting an issue.

@biofractal
Copy link

I have tried to write a plunker but I am afraid I could not get it working with angular. However it does show a simplified version of the problem: http://plnkr.co/edit/uJ7Dht4vxPZ83KHQx8q4

To be clear what the issue is: The $state.transitionTo('my-state') is not showing the template specified by the 'my-state'.

I can see the content is loading (debug output shows the template is fetched), I can even see that the browser flashes with what appears to be the correct content, but then it reverts to the original, pre-transition content.

So, if I display the 'home' content and then I run the following code:

if condition
    ev.preventDefault()
    $state.transitionTo('login')

Then the login template will load, flash briefly and then the browser will revert to displaying the previous home content.

I hope this helps.

Jonny

@sergeykosik
Copy link

Having the same problem as @biofractal mentioned. For the moment I had to move that condition check (securityService.isAuthenticated()) into $stateChangeSuccess event handler.

@geemang2000
Copy link

I tried and abandoned the $on approach and went to using the resolve closure instead. The reason: even tho you're capturing the state change event. Its too late to doing anything... the state change has started and the new page / controller have started loading... before my isAuthenticated AJAX check (in the $on handler) had finished.

Causing nullpointers, page flashes and more. Its not all the pretty but, the only reliable solution I've found.

something like...

resolve: {
    userSession: function(Session, $state) {
        return Session.validate().then(function(session) {
        if (!session) {
            $state.go('login.page1');
            return null;
         }
         return session;
         });

}
....

From what I can tell, resolve is the only reliable why to do real work before a page loads.

@jgrenon
Copy link

jgrenon commented Feb 10, 2014

I've just encountered this same problem with the latest 0.2.8 version. I was using $state.go inside the $stateChangeStart event handler with notify:false, to avoid infinite loop. The state was successfully changed, but the view was not rendered (location was changed).

The problem I guess is that $stateChangeSuccess isn't called if notify is false. After modifying the code :

angular-ui-router.js: 1821
if (options.notify) {
$rootScope.$broadcast('$stateChangeSuccess', to.self, toParams, from.self, fromParams);
}

To remove the if, everything was working as expected.
So option.notify should maybe only prevent another $stateChangeStart and not prevent success or error events to be broadcast?

@timkindberg
Copy link
Contributor

So option.notify should maybe only prevent another $stateChangeStart and not prevent success or error events to be broadcast?

Might be a good idea...

@darlanalves
Copy link

Same issue here!

Changing the options.notify conditional did not help, tho.
If event.preventDefault() is called and state.go() is called in the sequence with the same state, nothing happens on screen (the view is loaded, the router enters the state but ui-view does not updates the screen). When I activated the $stateChangeSuccess event, the router entered in a crazy loop that freezes the Chrome and don't even let me close the tab or the browser =S

The workaround by now to me is move the authentication to a mix of $stateChangeStart and a resolved promise. Also, I'm using 0.2.10. Current stable (0.2.8) brakes the entire app with another crazy loop! (call stack overflow)

@nateabele nateabele self-assigned this Apr 17, 2014
@mikehaas763
Copy link

Just wanted to add that I'm also having the same issue. There's all sorts of weird behaviors when I event.preventDefault() and then state.go('login'). I get an error about 10 $digest calls, because of the infinite loop I'm assuming. It seems to me the infinite loop only happens on my initial application load. I have an otherwise('/dashboard') and when I just visit example.com (html5mode) it hits an infinite loop. However if I visit example.com/dashboard the $stateChangeStart event is fired once, for Dashboard state, then again for the login state.

@masimplo
Copy link

masimplo commented May 8, 2014

I am having the same issue as well. In $stateChangeStart I do a event.preventDefault() and the $state.go('login', null, {notify:false});
I get to /login url but the view is not rendered. Removing notify: false of course works after executing $stateChangeStart more times.

@alonbardavid
Copy link

From what I can tell this issue resolves aroud two problems:

  • if not using preventDefault : $stateChangeStart event happens before the "global" $state.transition is set (to a local variable), so even though doing a state transition inside $stateChangeStart triggers a transition change, it get's overriden because $state.transition is later set to the previous value. (this is in state.js:873)
  • if using preventDefault in $stateChangeStart, then immediately the syncUrl function is called (in master it's now $urlRouter.update(), but it seems to be the same) which causes the location to change back to the original url.

I can see two solutions here, either set the $state.transition before calling $stateChangeStart (in which case the new transtionTo will work).
Or alternatively allow preventDefault to not sync the url to it's previous state (perhaps by some-kind of flag, or checking $state.transition or someting).

@mikehaas763
Copy link

@Illniyar I'll try out some things based on your research and post my findings back here.

@gfreeau
Copy link

gfreeau commented Jul 16, 2014

Here is what I ended up with.

The notify flag disables broadcasting of stateChangeStart and stateChangeSuccess. stateChangeSuccess is required for the correct operation of the router.

I got around this by manually broadcasting the stateChangeSuccess event.

In the ui router source code, in state.js, we need to change the options.notify flag to allow us to choose if we want we want to broadcast, both, none or one of these events. Perhaps something like a 'resume' or option, which works after the event.preventDefault() is called.

@nateabele What do you think?

Here is a really simple proof of concept: gfreeau@5579fe3

edit: I also found this discussion at #618, the latter comments are discussing what I'm talking about here.

i.e

$state.transitionTo(toState.name, toParams, {resume: true});

instead of (in my workaround code below)

$state.go(toState.name, toParams, {notify: false}).then(function() {
    $rootScope.$broadcast('$stateChangeSuccess', toState, toParams, fromState, fromParams);
});

options.resume would fire startChangeSuccess but not stateChangeStart.

Here is my current workaround for now. thanks to @homerjam and #1158.

.run(function ($rootScope, $state, $q, Auth, AUTH_EVENTS) {
        $rootScope.$on(AUTH_EVENTS.notAuthenticated, function(event) {
            console.log('not authenticated');
            // go to login state
        });

        $rootScope.$on(AUTH_EVENTS.notAuthorized, function(event) {
            console.log('not authorized');
        });

        $rootScope.$on('$stateChangeStart', function(event, toState, toParams, fromState, fromParams) {
            var stateData = toState.data || {};

            // my login state will early return here
            if (stateData.allowAnonymous) {
                return;
            }

            event.preventDefault();

            // checks the server if the user has a saved json web token
            // returns a promise, cached after first check
            Auth.check()
                .catch(function() {
                    $rootScope.$broadcast(AUTH_EVENTS.notAuthenticated);
                    return $q.reject('not authenticated');
                })
                .then(
                    function() {
                        // user authenticated, check authorization

                        var requiredRole = stateData.role;

                        if (requiredRole && !Auth.isAuthorized(requiredRole)) {
                            $rootScope.$broadcast(AUTH_EVENTS.notAuthorized);
                            return $q.reject('not authorized');
                        }
                    }
                )
                .then(
                    function() {
                        // user authenticated and authorized, activate the state

                        // $state.go(toState.name, toParams, {resume: true});

                       // workaround
                        $state.go(toState.name, toParams, {notify: false}).then(function() {
                            // line 907 state.js
                            $rootScope.$broadcast('$stateChangeSuccess', toState, toParams, fromState, fromParams);
                        });
                    }
                )
            ;
        });
    })

@bastienJS
Copy link

Are there any news on this? I also have to use the stateChangeStart event and call state.go() there doing a little computation and pass the return value as route parameter to the state.go() function. But I can not due to the above behavior. The ui is not rendered :-/

@bastienJS
Copy link

@gfreeau I am using your workaround and so far it works. Thank you very much!

@dirkpostma
Copy link

+1

@fubupc
Copy link

fubupc commented Aug 23, 2014

@gfreeau +1 , thank you!

@srnm
Copy link

srnm commented Sep 7, 2014

It appears that the release of v0.2.11 has no impact on this issue.
And that @gfreeau 's workaround is still required.

Can anyone confirm?

thx

@fubupc
Copy link

fubupc commented Sep 9, 2014

@gfreeau I found an issue in your workaround solution: if you have an config: $urlRouterProvider.when('/A', '/A/B') and then it will go to infinite loop when visit '/A' .

@fubupc
Copy link

fubupc commented Sep 9, 2014

I have made a small demo at: http://plnkr.co/edit/lUkJx27HiaaNAJbF07n2

If you click 'A' link then you will see angular error: "10 $digest() iterations reached. Aborting!".

@gfreeau
Copy link

gfreeau commented Oct 17, 2014

As a followup to my solution. Since the initial event is prevented. $state.transitionTo no longer returns the original promise, it returns a rejection. This means you cannot do this in the code:

$state.transitionTo(...).then(...)

The problem I am trying to solve is an initial application load of dynamic data before proceeding (such as making sure the current user is authenticated). According the the issues for AngularJS, it's possible angular may get an application wide resolve, meaning this work-around would no longer be needed.

For UI router, it could be possible to provide a promise to stateChangeStart event, so you can return your own promise once your custom logic is done. Right now, if you prevent the initial transition, UI router returns a rejection.

See angular/angular.js#5854

@robertjd
Copy link

+1 for being able to return a promise from a state change event handler.

I worked around this problem by de-registering the handler, before calling transitionTo:

var deRegisterInterceptor = $rootScope.$on('$stateChangeStart',
  function interceptor(e,toState,toParams){
    e.preventDefault();
    deRegisterInterceptor();
    // do stuff
    $rootScope.$state.transitionTo(toState.name,toParams);
  }
);

@crazyrussianboris
Copy link

Have same issue, but only in Chrome and Opera. All examples can't help to avoid this. Also i have the same problem just going through usual ui-sref link.

http://stackoverflow.com/questions/27491617/chrome-opera-blink-painting-issue

@pusherman
Copy link

@mikehaas763 did you ever find a way to resolve your problem? I'm experience the exact same behavior. It only happens when html5mode is true. The workaround provided by @gfreeau doesn't seem to work for me.

@gabriel-anglada
Copy link

Hi all! any news?

cjh1 added a commit to OpenChemistry/mongochemclient that referenced this issue May 14, 2015
We need to manually broadcast 'stateChangeSuccess'. See
angular-ui/ui-router#178 for
more details.
@atul221282
Copy link

Well workaround didn't work for me ao is there any news on fix

@eddiemonge
Copy link
Contributor

@darlanalves
Copy link

I ended up replacing the transitionTo() method in a decorator rather than preventing or refiring events. This way I could intercept any state transitions and deal with it in the way I need, only then calling the original method when it is allowed to continue.

@batteur
Copy link

batteur commented Aug 12, 2015

Got same issue. My case was related to using the $urlRouterProvider.otherwise in combination with the preventDefault and $state.go in a $stateChangeStart interceptor.

A workaround is found here: #600

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