Skip to content
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

Not possible to add state.resolve from $stateChangeStart event #1165

Closed
facultymatt opened this issue Jun 26, 2014 · 35 comments
Closed

Not possible to add state.resolve from $stateChangeStart event #1165

facultymatt opened this issue Jun 26, 2014 · 35 comments
Labels
Milestone

Comments

@facultymatt
Copy link

Overview:
Sometimes it's desirable to add state resolves on the fly, instead of needing to add them to all states beforehand. For example, a resolve that verifies a user is authenticated or fetches the user from an API.

It's possible to do this with $routeChangeSuccess using the following code:

// @see http://stackoverflow.com/a/18245378/1738217
function authenticate() {
    if ( user.isAuthenticated ) {
        return;
    }
    // simulate deferred
    return $timeout(function() {
        user.isAuthenticated = true;
    }, 3000);
}

$rootScope.$on( "$routeChangeStart", function( e, next ) {
    next.resolve = angular.extend( next.resolve || {}, {
        __authenticating__: authenticate
    });
});

However the same is not possible with $stateChangeSuccess. This is because the event is broadcast with to.self, see this line. This means if you add a resolve in the $stateChangeSuccess event, you are actually adding on to.self.resolve which is never considered for resolve.

_Related:_
#1153

@timkindberg
Copy link
Contributor

Seems like it could be handy.

@NicoleY77
Copy link

Same problem here. I can't add a custom resolve dynamically so I have to hack this way:

$stateProvider
     .state("home", {
           url: "/",
           resolve: {} // add {} default
})

@christopherthielen
Copy link
Contributor

Interestingly, resolve is one of the only fields on state that can't be decorated via the standard API. However, you can decorate it another, uglier way.

      // Need access to both state representations. Decorate any attribute to access private state object.
      $stateProvider.decorator('path', function(state, parentFn) {
        if (state.self.resolve === undefined) {
          state.self.resolve = {};
          state.resolve = state.self.resolve;
        }
        return parentFn(state);
      });

@elado
Copy link

elado commented Aug 29, 2014

👍 very useful.

I sometimes load additional resources (css/js/images..) before showing a route, making sure it's fully loaded. I'm switching now to ui-router and encountered this issue.

@elado
Copy link

elado commented Aug 29, 2014

As @NecoleYu wrote (thanks!), initializing the state definitions with resolve: {} gives access to the actual resolve object that is used internally by ui-router, and allows to add resolves within $stateChangeStart.

$stateProvider
  .state 'home',
    url: "/"
    resolve: {}

...

  $rootScope.$on '$stateChangeStart', (e, toState, toParams, fromState, fromParams) ->
    toState.resolve.x = ->
      $timeout ->
        alert "done"
      , 3000

but would be nicer not to need this.

@afc163
Copy link

afc163 commented Sep 3, 2014

@elado Agree

@christopherthielen christopherthielen added this to the future milestone Sep 4, 2014
@kureus
Copy link

kureus commented Sep 25, 2014

@elado Very useful follow up comment

@peterrobertz
Copy link

Oh that possibillity would be wonderful!

@goliney
Copy link

goliney commented Apr 3, 2015

Need it so much. Very handy. +1

@pmowrer
Copy link

pmowrer commented Apr 10, 2015

I'd love to see this as well. I wasn't able to make @christopherthielen's workaround work, though I was scratching my head as to why. Perhaps it depends on the nesting. Stepping through it, it appears the decoration has to happen before the state building is queued, for whatever reason. No hooks to work with there so for now I wrapped $stateProvider.state, decorating the config before passing it onto the original implementation. If anyone has a better solution I'd love to hear it.

@hoeni
Copy link

hoeni commented Apr 29, 2015

+1. That would be really helpful!

@NilsEngelbach
Copy link

also struggeld with this problem because some states had a defined resolve object and some did not.
Took me forever to figure out why this was not working with all states:

if (!toState.resolve) { toState.resolve = {} };
toState.resolve.pauseStateChange = [ '$q', ($q: ng.IQService) => {...})];

Now i use the decorator suggested by @christopherthielen ! Thanks :)

But it should be possible to extend the resolve method without this "hack"! 👍

@alexweber
Copy link

Thanks all for the comments, this is gold! :) 👍

@christopherthielen
Copy link
Contributor

We've added this capability to the 1.0 code:

https://github.com/angular-ui/ui-router/blob/feature-1.0/src/transition/transition.ts#L162-L172

app.run(function ($transitions) {
  // register global transition start hook
  $transitions.onStart({ from: '*', to: '*' }, function($transition$) { // inject the Transition instance
    $transition$.addResolves({
      newResolve: function(MyService) {
        return MyService.someAsyncThing();
      }
    });
  });
});

Alternatively, if you just want to delay the transition, you can return a promise from the hook:

app.run(function ($transitions) {
  // register global transition start hook
  $transitions.onStart({ from: '*', to: '*' }, function(MyService, $q) {
    return MyService.waitForAsyncThing().then(function(data) {
      if (data.failure) return $q.reject("Failed thing, cancel transition");
    });
  });
});

@kmccullough
Copy link

@christopherthielen I tried to use this method for ACL, but I'm running into a problem prevent route change if route/state rejected.

If I use onStart, I can't access $state$ (toState). $state$ is not exposed and error occurs.
If I use onEnter, then I don't seem to be able to prevent the route from changing.

    $transitions.onEnter({ from: '*', to: '*' }, function($state$, $q, acl) {
        var requiredPermissions = getRequiredPermissions($state$);
        return acl.ready().then(function(acl) {
            if (!acl.can(requiredPermissions)) {
                // Rejection seems to prevent individual state/view change,
                // but doesn't prevent route change
                return $q.reject({ reason: 'acl' });
            }
        });
    });

I haven't tried using addResolves yet. I previously had this working using ngRoute with resolves. The transition handler seems to be more appropriate for this case.

@kmccullough
Copy link

Tried using addResolves, but that doesn't seem to be doing it either:

        $transition$.addResolves({
            controllerAcl: function() {
                console.log('running resolve');
                var deferred = $q.defer();
                acl.ready(function(acl) {
                    if (acl.can(requiredPermissions)) {
                        console.log('resolved');
                        deferred.resolve(toState);
                    } else {
                        console.log('rejected');
                        deferred.reject({ reason: 'acl' });
                    }
                });
                return deferred.promise;
            }
        });

I get no console logs.

@christopherthielen
Copy link
Contributor

If I use onStart, I can't access $state$ (toState). $state$ is not exposed and error occurs.

You should inject $transition$ and use var toState = $transition$.to()

If I use onEnter, then I don't seem to be able to prevent the route from changing.

Odd, that should stop the state change...

edit: yes, it should stop the state change. Check the console trace: http://plnkr.co/edit/6hWLzBGawc8VcjKvNGHi?p=preview

@kmccullough
Copy link

@christopherthielen

should inject $transition$ and use var toState = $transition$.to()

Yep, that works. Though, now I'm wondering why to/from are functions.

Odd, that should stop the state change...

After some testing I found that if I use ui-sref then the route properly never changes to the rejected route, but when I use href or just type the rejected route into the location bar, the state change is rejected (views remain on previous states), but the URI in the location bar remains the rejected route. I would have expected the previously valid route to overwrite the rejected URI in the location bar. Is there another step I need to do to prevent directly navigating to invalid routes (those where I return a reject from onStart)?

@kmccullough
Copy link

Also, $stateChangeError doesn't fire when a rejection is returned from onStart

@christopherthielen
Copy link
Contributor

Yep, that works. Though, now I'm wondering why to/from are functions.

The to/from object defines the Transition-matching criteria. They are used to determine if the hook should be applied to a transition or not. You can use a glob or a function. The function can inspect the to state or from state to determine if the hook should be applied to the transition.

See how this hook works: https://github.com/ui-router/sample-app/blob/master/app/routerhooks/redirectTo.js

when I use href or just type the rejected route into the location bar, the state change is rejected (views remain on previous states), but the URI in the location bar remains the rejected route.

I think you are describing #2455 which is fixed in master

Also, $stateChangeError doesn't fire when a rejection is returned from onStart

Did you re-enable the deprecated state events by adding ng1/stateEvents.js?

See the release notes for what I'm referring to: https://github.com/angular-ui/ui-router/releases/tag/1.0.0alpha0

All state events, (i.e. $stateChange* and friends) are deprecated and disabled by default; re-enable them by including the ng1/stateEvents.js file, and depend on the ui.router.state.events angular module in your application.

@kmccullough
Copy link

The to/from object defines the Transition-matching criteria.

Unless I misunderstand your answer, I didn't mean the matching criteria ({ to: '', from: '' }), but rather the to() method on injectable $transition$; why $transition$.to() and not just an object at $transition$.to?

I think you are describing #2455 which is fixed in master

I'm not sure how to access that fix, unless it gets merged into 1.0.0alpha0. In any case, I made the modification specified in 25e0c04 locally, but doesn't fix either use case of HREF on an anchor or direct URL manipulation in the address bar. In both cases, the rejected URL remains in the address bar.

I noticed another side effect: after I click on an HREF link to a rejected route, the address bar changes to the new route and the current state remains, but then if I click on a UI-SREF link to go to the state I'm currently on (though the address bar says otherwise) the address bar doesn't change back to the current valid route, because it thinks I'm already there, which also raises an onError. If I refresh while the rejected route is in the address bar, I'll get no view at all (I'd expect the "otherwise" route).

Did you re-enable the deprecated state events by adding ng1/stateEvents.js?

Nope. You're right. I read that right after I posted.

It seems the alternative is using onError along with $transition$.error(), but $transition$.error() is just giving me undefined and I need to be able to access the passed rejection object.

@christopherthielen
Copy link
Contributor

why $transition$.to() and not just an object at $transition$.to?

Oh, I see... it's just an accessor method so we can change where the value comes from in the future without disrupting the API.

I'm not sure how to access that fix, unless it gets merged into 1.0.0alpha0.

It's merged into 1.0 because master is the 1.0 code. The 0.2.x code is now in legacy

doesn't fix either use case of HREF on an anchor or direct URL manipulation in the address bar. In both cases, the rejected URL remains in the address bar.

Ah yes, you're right. Would you mind opening a new ticket for that issue? Here's a plunk http://plnkr.co/edit/9uVnia9Vikx6Dcxgaz1P?p=preview

$transition$.error() is just giving me undefined and I need to be able to access the passed rejection object.

Transition.error() is related to Transition.valid(). When a Transition is invalid (it means the transition cannot even be started), .error() returns a message explaining why the object is invalid and could not be started. That is different from a valid transition which has started, but been rejected due to rejecgted hooks or resolves.
http://angular-ui.github.io/ui-router/feature-1.0/classes/transition.transition-1.html#error


I see you're implementing a global ACL. One of the primary drivers for the design of the transition hooks was adding custom global behaviors like this. I'd like the hooks to be sensible for you and your use case, and if they are not, I want to understand where the gaps are. Where would you like to check for your ACL transition rejection?

In that same plunk I added a global error hook: http://plnkr.co/edit/9uVnia9Vikx6Dcxgaz1P?p=preview

  $transitions.onError({ }, function($error$) {
    console.log("captured error: ", $error$);
  });```

@kmccullough
Copy link

It's merged into 1.0 because master is the 1.0 code. The 0.2.x code is now in legacy

Forgive me, I'm fairly new to git, so what exactly should I tell bower to download, because I tried master, but it just gives me the 4 files for "bower package manager support". These are the options it gives me:

Available versions in git://github.com/angular-ui/angular-ui-router-bower.git: 0.2.18, 0.2.17, 0.2.16, 0.2.15, 0.2.14, 0.2.13, 0.2.12, 0.2.11, 0.2.10, 0.2.9, 0.2.8, 0.2.8-bowratic-tedium, 0.2.7, 0.2.6, 0.2.5, 0.2.0, 0.0.1

.

Would you mind opening a new ticket for that issue?

#2611

In that same plunk I added a global error hook

It looks like your plunk rejects with { reason: 'nope' }, but $error$ in onError contains {type: 5, message: "The transition was ignored.", detail: undefined}. I need that rejection object/message in $error$, but if $error$ is already a useful thing, then perhaps a property on it, such as $error$.rejection, which would contain either whatever is passed to $q.reject() or whatever else can be returned from an onEnter/onStart to cancel it.

In my current use case I have two separate hooks corresponding to two different contexts of ACL, but they both run and both could fail, and either failure requires the same redirect, and if both fail at the same time I don't want two redirects to occur. Previously, when using $stateChangeError, it would be called only once and I could check which one of the failures was raised, and redirect once. So, will onError be called once with the error that caused it, or will it be called once for every onStart/onEnter that rejected? If it is called more than once then I'd have to somehow make sure there isn't a double redirect.

@christopherthielen
Copy link
Contributor

what exactly should I tell bower to download

1.0.0alpha0 is a released build. We don't publish daily snapshots, so you'd have to build it yourself

git clone https://github.com/angular-ui/ui-router.git
cd ui-router
npm install
grunt build

Then use angular-ui-router.js from build/ (By the way, I recommend switching from bower to npm)


It looks like your plunk rejects with { reason: 'nope' }, but $error$ in onError contains {type: 5, message: "The transition was ignored.", detail: undefined}.

Click the link labelled "Click this Link" to see the onEnter rejection happen.


So, will onError be called once with the error that caused it, or will it be called once for every onStart/onEnter that rejected?

The hooks are run as a chain, in a specific order (onBefore -> onStart -> onExit -> onRetain -> onEnter -> onFinish -> onSuccess/onError). A transition can only be rejected by one hook.

@kmccullough
Copy link

By the way, I recommend switching from bower to npm

I do use npm for tools and libraries related to building my app, but I use bower for libraries that will be actually included in the app, because bower offers the main property, which allows me to bower install and then just use it without having to find which file needs to be included and manually adding an include (https://github.com/ck86/main-bower-files). AFAIK the main property in package.json doesn't allow an array and typically points to a nodejs file (whereas I'm doing JS in the browser). If you know of a way to switch from bower while maintaining this automation functionality, that would be great; I'm not married to bower otherwise.

Click the link labelled "Click this Link" to see the onEnter rejection happen.

Yeah, I must have been looking at the ignore error for the state that was already applied. I do see that $error$ is decorated with the rejection object. This will work fine for my purposes, but for others: what if the rejection is a string (or something else), like how many angular examples show $q.reject('some message), how do you access the value then?

The hooks are run as a chain, in a specific order (onBefore -> onStart -> onExit -> onRetain -> onEnter -> onFinish -> onSuccess/onError). A transition can only be rejected by one hook.

So, I take that to mean that if one onEnter rejects a route, any other onEnter's (for the same route/state change) won't even run. That's fine.

@christopherthielen
Copy link
Contributor

If you know of a way to switch from bower while maintaining this automation functionality

I've moved to ES6 modules and a bundler (webpack, etc), but now we're quite off topic ;)

what if the rejection is a string (or something else), like how many angular examples show $q.reject('some message), how do you access the value then?

The same way: as $error$. The transition rejection (whatever it is) is provided to the onError hook as $error$

So, I take that to mean that if one onEnter rejects a route, any other onEnter's (for the same route/state change) won't even run.

Correct

@christopherthielen
Copy link
Contributor

Closing since adding resolves on the fly is possible in 1.0

@stephengardner
Copy link

Can you please post an example using .addResolvable since .addResolves doesn't appear to be available anymore

@christopherthielen
Copy link
Contributor

christopherthielen commented Jan 10, 2017

@stephengardner

See: https://ui-router.github.io/ng1/docs/latest/classes/transition.transition-1.html#addresolvable

transitionService.onBefore({ to: state => state.data.dostuff }, trans => {
  trans.addResolvable({ token: 'myResolve', deps: ['MyService'], resolveFn: MyService => MyService.getStuff() });
});

@stephengardner
Copy link

@christopherthielen thank you! That example helps! I think I was frustrated and had to bounce around to see the constructor implementation and have a tough time reading the font face or something. Still haven't quite figured out how to navigate everything. Probably my impatience

@christopherthielen
Copy link
Contributor

christopherthielen commented Jan 12, 2017

@stephengardner in the past, you HAD to use the Resolvable constructor, but that's not good API for end users:

import { Resolvable } from "angular-ui-router";

...
transition.addResolvable(new Resolvable('foo', ...));

In rc.1 I remembered to allow addResolvable to take a ResolvableLiteral which is a plain object, as shown above. 👍

I'll add an example to the docs. ui-router/core@6d41c31

@stephengardner
Copy link

Adding my route authentication as an example, Thanks again for the clarification @christopherthielen
This might help someone who was in my position.

/**
 * Authentication Resolvable - if I add  "authenticate" to a route params, this will make sure the user is logged in
 * before proceeding.  
 */
$transitions.onStart({}, (trans) => { // on any route, do the following...
  let next = trans.to(); // get the state and its params
  if (next.authenticate) {
    trans.addResolvable({
      token : 'authResolve', // the token can be anything, here, we never actually use it in this specific case
      deps : ['$state', 'Auth', '$q'], // inject what I need
      resolveFn: ($state, Auth, $q) => {
        return new $q((resolve, reject) => { // make sure this is a promise, in order to delay the route
          let onUserLoggedIn = () => {
            console.log('[Auth Service] the necessary *authenticate* state PASSED the resolvable, continue on!');
            return resolve();
          };
          let onUserLoggedOut = () => {
            console.log('[Auth Service] the necessary *authenticate* state FAILED the resolvable, going to login page!');
            $state.go('login');
            return reject();
          };
          // this can be substituted for any promise, observable, or async operation, here I'm using an observable
          Auth.isLoggedInAsync$().subscribe((isLoggedIn) => {
            if(!isLoggedIn) return onUserLoggedOut();
            return onUserLoggedIn();
          })
        })
      }
    }); }
});

@arashsoft
Copy link

arashsoft commented Oct 17, 2017

@christopherthielen Thank you for the example. I use same code like this:

transitionService.onBefore({ to: state => state.data.dostuff }, trans => {
  trans.addResolvable({ token: 'myResolve', deps: ['MyService'], resolveFn: MyService => MyService.getStuff() });
});

It works fine but when I try to add 'myResolve' as a dependency to my page controller, it is undefined (and I am sure I resolve the promise to a value).

@christopherthielen
Copy link
Contributor

@arashsoft is the "page controller" a routed controller? In other words, is it found inside a state definition?

@arashsoft
Copy link

@christopherthielen Yes it is the state controller. I will create a plunker and send it here.

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