Skip to content

$stateChangeStart to wait for an ajax request (some manual promise to ressolve) without state.go() #1399

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
syrys opened this issue Sep 24, 2014 · 64 comments
Labels

Comments

@syrys
Copy link

syrys commented Sep 24, 2014

Got a quick question for you guys.

I want to be able to execute a possible ajax request from a factory on "stateChangeStart" event (i check some state parameters). Seems like if i event.preventDefault(), then call the factoryMethod for the ajax request, then on resolve do a state.go() to continue into the state, it simply gets caught in the same conditions and ends in an infinite loop. Obviously i can set some other flags somewhere to prevent this, but is there a better way to do it than this? Some alternative to state.go()?

Also tried { notify: false }, but that skips "$stateChangeSuccess" also. I want "$stateChangeStart" called only once and "$stateChangeSuccess" called only once, unless you get redirected to another page on fail condition of the promise (then it should call "$stateChangeStart" for the new state as usual).

Note the stateChangeStart is on the root controller.

Ps. im trying to check if a local user object exists and if not, it will check a rest api to see if a user session exists and fetches the user data. This is so i dont have to put ressolve methods on every state, instead a simple flag indicating that the user must be logged in to view this state.

Thanks in advance.

@syrys
Copy link
Author

syrys commented Sep 25, 2014

seems like the same/similar case has been discussed in couple of threads but i dont yet see an elegant solution.

Best i have seen is the one that was proposed at #1158

Has anything been changed since? Is there any plan of implementing extra event functionality maybe (event.pause(), event.resume(), or even state.continue(toState): simply continues without triggering stateChangeStart event?).

@sricc
Copy link

sricc commented Sep 25, 2014

I was just about to ask this same exact question... 👍

@nateabele
Copy link
Contributor

We will have a thing for this in the next release. Hang tight.

@christopherthielen
Copy link
Contributor

the next major release, FYI

@syrys
Copy link
Author

syrys commented Sep 25, 2014

Sweet, looking forward to it. Is there a rough ETA by any chance?

@christopherthielen
Copy link
Contributor

less than a year 👍

but seriously, we're getting close to a preview release. I'd say within the month.

@sricc
Copy link

sricc commented Sep 25, 2014

That is great news... looking forward to it! Thanks.

@syrys
Copy link
Author

syrys commented Sep 25, 2014

great, sounds good. Hopefully we will hear about it soon :)

@sparty02
Copy link

sparty02 commented Nov 4, 2014

@nateabele @christopherthielen Is there an issue open to document this feature that will be 'in the next release'? I'm curious as to whether it will solve a similar problem I have.

@christopherthielen
Copy link
Contributor

A month ago I said a preview would be ready in a month, but that obviously hasn't happened yet. For now, I would suggest reading the source code: https://github.com/nateabele/ui-router/blob/new/src/transition.js

@sparty02
Copy link

sparty02 commented Nov 4, 2014

@christopherthielen Very nice! I like the to/from glob support to help solve the problem of applying these transition hooks to more than one state. Along these lines, it appears that these kind of hooks would happen in the config block against the transitionProvider, vs as part of the stateProvider config, right?

@sparty02
Copy link

sparty02 commented Nov 4, 2014

Something like this?

$transitionProvider.on(
    {
        "from" : "home",
        "to" : "admin*"
    },
    {
        "auth" : function ($http) {
            return $http.get('authorize').then(function (response) {
                return response.data.authorizations.admin === true;
            });
        }
    }
);

@christopherthielen
Copy link
Contributor

these kind of hooks would happen in the config block against the transitionProvider

correct.


The callback has to be a function. That function can return a promise, false, a redirect Transition, or additional resolves

$transitionProvider.on( { "from" : "home", "to" : "admin*"  },
    function($http) {
        return $http.get('authorize').then(function (response) {
            return response.data.authorizations.admin === true;
        });
    }
);

@christopherthielen
Copy link
Contributor

Also, both to and from criteria are optional and can be functions with a state parameter. With this you can annotate your states and do operations based on the state annotations.

something like this:

$transitionProvider.on( { 
      to: function(state) { return state.requiredRole !== undefined;  } 
    },
    function($http, $transition$) {
      var requiredRole = $transition$.to.state.requiredRole;
      return $http.get('authorize').then(function (response) {
        return response.data.authorizations[requiredRole] === true;
      });
    }
);

@sparty02
Copy link

sparty02 commented Nov 4, 2014

This is killer.... thanks!

@gfreeau
Copy link

gfreeau commented Nov 12, 2014

@christopherthielen, just looking for some idea of when you think a release or pre-release would be available. I noticed you have been making a lot of changes for the typed parameters and the transitionProvider changes are in another branch. I am looking forward to the changes and I appreciate your time, some kind of timeline when all the features would be merged and ready for testing would be great, I've been patching UI router for use in a current project.

These changes resolve the issues of a workaround I made in #178 and many similar issues.

@christopherthielen
Copy link
Contributor

@gfreeau 0.2.12 with all the typed/optional params fixes is nearly ready. I think once I clean up some documentation, I will release it. The timeline for the 1.0.0 preview is a little vague. I'm waiting on some code @nateabele wrote integrating the updated view pipeline, but he's been busy traveling. Once his view pipeline code is complete, I am going to get 1.0.0 routing and ready for a preview release. I wish I could give a better answer than that.

@gfreeau
Copy link

gfreeau commented Nov 14, 2014

@christopherthielen is the only major changes between master and the branch with TransitionProvider the changes you have been working on regarding typed params?

@christopherthielen
Copy link
Contributor

@gfreeau I'm periodically merging the master branch into nateabele/new to catch that branch up with the bugfixes and features in 0.2.12. The new branch has quite a bit of other refactoring that is not TransitionProvider code.

@gfreeau
Copy link

gfreeau commented Nov 14, 2014

@christopherthielen, the new state.js looks like it's completely overhauled. I can see what you are trying to achieve, it will be a very powerful change with the transitionSteps and callbacks, especially for use cases such as auth checks and role checks. For now I don't see how to begin testing with it (in addition to using your changes to typed params), I was hoping that I could patch the latest 0.2.12, however it seems you need a deep understanding of the changes made and internals before that could be possible.

The new transitionprovider is straight forward, how its use in state.js along with the other changes made will take some time to understand, it looks like a difficult task to merge everything. I will be glad to test out this feature, when any kind of merge happens. Right now, I have to use hacks such as the ones in #178

@christopherthielen
Copy link
Contributor

@gfreeau We will continue to sync new with master until new is ready to be published. At that time, I'll branch master to some legacy "0.3" branch and merge the entirety of new into master (essentially new is the defacto HEAD at the moment, with 0.2.x being merged into it).

it will be a very powerful change with the transitionSteps and callbacks, especially for use cases such as auth checks and role checks

Yes, I'm looking forward to these capabilities too!

For now I don't see how to begin testing with it (in addition to using your changes to typed params), I was hoping that I could patch the latest 0.2.12

Yeah, I don't have a plan for merging new bit by bit into master because new is really an overhaul to address some fundamental architecture problems we've noted. Unfortunately, that means we have to wait for the view sync code before we can start banging on new

@ProLoser
Copy link
Member

ProLoser commented Dec 7, 2014

@christopherthielen just curious, how is this $transitionProvider better than resolves?

@christopherthielen
Copy link
Contributor

@ProLoser transitionProvider doesn't replace resolves; they are still an important concept. TransitionProvider hooks are a more flexible superset of resolves and onEnter/onExit.

$transitionProvider's callbacks are hooks for transition-scoped events. The hook criteria can be functional, or glob for both the to/from state for the transition. The result of a hook can alter the transition completely (abort/redirect), or it can register additional resolvables/promises for the transition. This flexibility can be used in many ways.

We can implement logic like "default substate of abstract state" easily by checking if the to state is marked as abstract, and returning a redirect.
Something along these lines:

function isAbstract(state) { return state.abstract !== undefined; }
function redirectToDefaultSubstate($transition$) { return $transition$.redirect($transition.to.state.abstract); }

.on({ to: isAbstract }, redirectToDefaultSubstate);

We can implement promise-based confirms when exiting a state

function confirm($transition$, Data, Dialogs) { 
  if (Data.dirty) { 
    return Dialogs.confirm("Ya sure?"); // adds this promise to the current promise chain
  } 
}
.exiting({ from: 'my.state.*` }, confirm);

TransitionProvider generalizes the resolve concept and improves flexibility. A resolve is functionally roughly equivalent to registering a resolve during the 'entering' event.

var resolves = { myResolve: function(Service) { return Service.promise(); } }
.entering({ to: 'my.state'}, function() { 
  return resolves;  // adds key/value resolve/resolvefn to current Resolvables
} );

@schmod
Copy link

schmod commented Dec 16, 2014

Could this functionality theoretically be used to resolve custom UrlMatcher types?

@paulflo150
Copy link

Any thoughts on when this feature will be available?

Thanks!

@ludovicMercier
Copy link

Does the new Transitionprovider give the possibility to customize the dialogbox by the use of $modal service in place of a standard confirm("are your sure ?") ?

@lebster
Copy link

lebster commented Mar 17, 2015

+1

@nateabele
Copy link
Contributor

@paulflo150 Soon. :-)

@lvarayut
Copy link

+1 Any progress on this issue?

@nateabele
Copy link
Contributor

@arjunasuresh3
Copy link

+1

@gergelyhegykozi
Copy link

I had similar problems, so made a flexible provider: https://github.com/leonuh/ui-router.redirect - I hope it can help you to handle the dependecies and chains more efficiently until the new ui-router :)

@slavafomin
Copy link

Hello! I'm also looking for a way to do asynchronous operations inside of UI router's event handlers. I've tried to return promise from the event handler, but it's not respected by the router. Are there any features available right now in production, that I can use to achieve the similar result? Thank you!

@nateabele
Copy link
Contributor

I've tried to return promise from the event handler, but it's not respected by the router.

Correct, UI-Router's events are standard Angular events, and so do not support anything promise-related.

Are there any features available right now in production

Not in production, but we're about to drop an alpha-quality version of 1.0, so, soon. 😄

@slavafomin
Copy link

Thank you Nate! I will be looking forward for those features.

In the meantime, I've created a top-level non-abstract state and put my global initialization logic inside of it's resolve property. That way it will be executed before anything else happens. I.e., I can restore the session using cookie variable and load user data from the server, before proceeding to other states. Maybe this idea will be useful to someone else here.

Cheers! ;)

@slavafomin
Copy link

Looks like my solution is not working as expected. When I do event.preventDefault() in $stateChangeStart the topmost state is not resolved and initialization doesn't happen.

I've tried approach mentioned by @syrys, i.e. doing event.preventDefault(), then my async operation and then $state.go, but I'm getting into indefinite loop that way. I've tried to disable notify flag. That works, but state's view is not rendered that way (looks like it's using some event internally).

I've tried this approach:

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

        if (toParams.skipSomeAsync) {
          return;
        }

        // We need to prevent navigation right now, in order to give time for
        // our asynchronous operations to proceed.
        event.preventDefault();

        doSomeAsync().finally(continueNavigation);

        function continueNavigation () {
          var params = angular.copy(toParams);
          params.skipSomeAsync = true;
          $state.go(toState.name, params);
        }

      });

But when $stateChangeStart event handler is called for the second time, I'm not getting skipAuthCheck param. Looks like router only passes pre-defined parameters.

@nateabele could you recommend some workaround please? How do I prevent second execution of my async logic and to break this indefinite loop?

P/S: I mean without creating a global flag: I prefer things isolated and encapsulated.

@garhbod
Copy link

garhbod commented Nov 25, 2015

@slavafomin I was trying to do the same thing by adding a new param but realised that a state can only contain the params that are in it's url url: "/edit/{id:[0-9]+}"

So what I did was created a variable, set to false, outside of the on $stateChangeStart and changed it to true where required.

var skipSomeAsync = false;
$rootScope.$on('$stateChangeStart', function (event, toState, toParams, fromState, fromParams) {

        if (skipSomeAsync) {
          return;
        }

        // We need to prevent navigation right now, in order to give time for
        // our asynchronous operations to proceed.
        event.preventDefault();

        doSomeAsync().finally(continueNavigation);

        function continueNavigation () {
          var params = angular.copy(toParams);
          skipSomeAsync = true;
          $state.go(toState.name, params);
        }
        skipSomeAsync = false;

      });

@garhbod
Copy link

garhbod commented Nov 25, 2015

Better yet instead of var skipSomeAsync make it $rootScope.skipSomeAsync so that it can be accessed elsewhere if required

@derekgreer
Copy link

It's been about 3 months since any talk of a release. Any update?

@nateabele
Copy link
Contributor

@derekgreer
Copy link

@garhbod Another approach that works:

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

        if (fromParams.skipSomeAsync) {
          return;
        }

        event.preventDefault();

        doSomeAsync().finally(continueNavigation);

        function continueNavigation () {
          fromParams.skipSomeAsync = true;
          $state.go(toState.name, toParams);
        }

      });

@slavafomin
Copy link

Be advised, that if you are using preventDefault() approach, the initial caller, e.g. $state.go will be rejected.

@tangorri
Copy link

@slavafomin that means there is no solution until version 1.0 ?

@slavafomin
Copy link

That's up to you and your app. It could be fine to use if you are not relying on the promise returned by "go".

@tangorri
Copy link

Well it seems the flag workaround works for me. thanks guys, when is 1.0.0 expected to be released ?

@jforjava1981
Copy link

@christopherthielen : I have a case where I have to load some configuration data from Back-End using http when anular app loads for the first time. What I have done right now is that I have created an abstract state which is inherited by all states. I load this configuration on parent state's resolve and provide the key to all states so that whenever a state is activated it first waits for parent's resolve to be satisfied even if user directly places a URL in browser. Is there a better way to do this? Can this feature solve my problem of having to inject parent resolve key into every state and if the configuration doesn't load how to redirect it to error state.

I have another case where I have to load some data from backend when I move from state a to state b or even if I directly place the b's URL in the browser. Depending on data I have to prevent the transition to state B. I believe this feature does exactly that. Correct?

@nirsmadar
Copy link

+1

@christopherthielen
Copy link
Contributor

@jforjava1981

I believe this feature does exactly that. Correct?

Yes, this feature allows you to implement all those cases without using resolve:. In 1.0, we are going to recommend resolve is used only for loading data.

@jforjava1981
Copy link

@christopherthielen Thanks. Waiting eagerly :)

@enriquevr
Copy link

@slavafomin In order to avoid the rejection of the promise caused by calling event.preventDefault I'm thinking in adding the async permission check into a factory method whose interface returns a promise and use that into both, the $stateChangeStart listener and in a decorated (using angular.decorator) version of $state.go which may or may not delegate to the original $state.go depending on the outcome of the check, I will report back on how it goes

@stephengardner
Copy link

1.0 is out. Anyone have an example of this? The docs are an absolutely tremendous task to understand without ample examples.

@schmod
Copy link

schmod commented Jan 3, 2017

1.0 isn't quite out yet. It's still marked as a beta.

@christopherthielen
Copy link
Contributor

1.0 is in release candidate: https://ui-router.github.io/blog/angular-ui-router-1.0.0-rc.1/

Start using it!

@christopherthielen
Copy link
Contributor

@stephengardner I spent quite a bit of effort cleaning up the docs. I hid most of the internal API by default. Hope they're easier to use now.

https://ui-router.github.io/ng1/docs/latest/index.html

@ProLoser
Copy link
Member

@christopherthielen I think there's a typo in the first example: https://ui-router.github.io/ng1/docs/latest/modules/directives.html#uistate it should be ui-state not ui-sref?

@christopherthielen
Copy link
Contributor

@ProLoser yep, thanks for pointing that out!

@ShurikAg
Copy link

ShurikAg commented Apr 8, 2019

@garhbod Another approach that works:

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

        if (fromParams.skipSomeAsync) {
          return;
        }

        event.preventDefault();

        doSomeAsync().finally(continueNavigation);

        function continueNavigation () {
          fromParams.skipSomeAsync = true;
          $state.go(toState.name, toParams);
        }

      });

Hi, this approach does not work (at least not anymore.
The fromParams are not extendible so in different browsers I am getting a different error.
For Safari:

TypeError: Attempted to assign to readonly property.
  at action(./app/features/checklist/checklist-ctrl.js:517:33)
  at fn(./app/features/message-box/confirm-ctrl.js:13:53)
  at expensiveCheckFn(./node_modules/angular/angular.js:16213:1)
  at $element(./node_modules/angular/angular.js:26592:1)
  at $apply(./node_modules/angular/angular.js:18094:1)
  at ? (./node_modules/angular/angular.js:26597:1)
  at dispatch(/npm/jquery/dist/jquery.min.js:2:41777)
  at s(/login:5:23562)
  at _a(./node_modules/@sentry/browser/dist/index.js:3257:1)

How can I work around it

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

No branches or pull requests