Skip to content

Added a $stateNotFound event which can prevent or redirect state transitions. #414

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

Merged
merged 3 commits into from
Sep 16, 2013

Conversation

eahutchins
Copy link

Added a $stateNotFound event which can be prevented (in which case transition aborted is returned with no error thrown), or redirected by substituting the redirect event argument with the desired target state and params. In the latter case the state lookup is always retried once (even when not redirected), potentially allowing lazy-definition of state.

The $stateNotFound handler looks like:

$scope.$on('$stateNotFound', 
function(event, redirect, fromState, fromParams){ ... })

where redirect contains the original to, toParams and options arguments to transitionTo, allowing any/all of those arguments to be altered.

@laurelnaiad
Copy link

Nice. It would be cool to do the parallel behavior in urlRouter to support url based laziness.

@eahutchins
Copy link
Author

$urlRouterProvider already has an otherwise() which does the trick, it can take either a string or a rule (which should be documented @timkindberg). This change makes it safe to use ui-sref for lazy states.

@laurelnaiad
Copy link

I guess thats true if one's otherwise rule for laziness does its own version of retrying once and only once.

@eahutchins
Copy link
Author

@nateabele could you give this a quick once-over to see if it looks about right (or grossly wrong)? I need to get ui-srefs working at our end...

@timkindberg
Copy link
Contributor

Added docs for rule as param with otherwise(). https://github.com/angular-ui/ui-router/wiki/URL-Routing#otherwise-for-invalid-routes

@timkindberg
Copy link
Contributor

I'd love to see some more example code on how you might use the handler for real. The redirect param is a tad confusing to me.

@nateabele
Copy link
Contributor

@eahutchins At first pass I was pretty much ready to merge it, but I spent another couple hours chewing on it, and I have three thoughts.

I pulled down and applied the patch and started playing with it a bit. I kind of started questioning the need for a redirect parameter when we already have an existing, well-understood pattern for superseding transitions.

I took one of your tests and rewrote it a bit, and the end result was effectively the same. Take a look and let me know what you think about this style:

it('can be redirected in $stateNotFound', inject(function ($state, $q, $rootScope) {
  initStateTo(A);

  $rootScope.$on('$stateNotFound', function ($event) {
    $state.transitionTo(D, { x: '1', y: '2' });
    $event.preventDefault();
  });

  var promise = $state.transitionTo('never_defined', { z: 3 });
  $q.flush();

  expect($state.current).toBe(D);
  expect($state.params).toEqual({ x: '1', y: '2' });
}));

Also, as @timkindberg mentioned, I'd very much like to see an example of how you're using this to do lazy-loading, since event-handling is synchronous (for my own understanding as well as the docs). If this is only a stepping-stone to the goal, let's work on figuring out how we can give $stateNotFound the ability to defer the transition until the target state is loaded.

Finally, since (well, if) we're making everything fully asynchronous, I wonder if it'd make more sense to convert the remainder of the errors to transition rejections. I can see arguments on both sides, but it really comes down to whatever we discover for deferred loading of states.

Anyway, looking forward to your thoughts. I think we're really close to a really clean solution, and at this point, this problem has become important enough that I'm holding the next release until we can solve it.

@eahutchins
Copy link
Author

@timkindberg, @nateabele I added a simple lazy-definition test case, it turns out in our case we're currently bouncing through $location.path() once we've detected the undefined state/route (we maintain a consistent naming scheme between states & URLs for our routable states). Deferring the transition retry on a promise somehow returned by the $stateNotFound handler would probably allow us to clean things up quite a bit (i.e. defer the state definition until a server round-trip has happened). I think eager definition in the actual handler could still be useful in some cases too. Ideally if there is a way to defer a transition you should still be able to get a TransitionSuperseded if another transition is requested (which might get complicated).

@laurelnaiad
Copy link

What if the event included a parameter that is, by default, an empty function... and what if event handlers could overwrite the empty function with their own function... and that function were then called by transitionTo using $q.when...?

The idea being that the event handler can insert a promise into the processing and have that promise respected by transitionTo prior to retrying.

@eahutchins
Copy link
Author

@stu-salsbury you mean a function returning a promise? Presumably this is only happening from the immediately prior event call so why not just put a promise into the event itself?

I could see something like this sort-of working:

if (!isDefined(toState)) {
    // Broadcast not found event and abort the transition if prevented
    var redirect = { to: to, toParams: toParams, options: options };
    evt = $rootScope.$broadcast('$stateNotFound', redirect, from.self, fromParams);
    if (evt.defaultPrevented) return TransitionAborted;
    if (evt.retryPromise) {
        var transition = $state.transition = evt.retryPromise;
        evt.retryPromise.then(function() {
            if (transition !== $state.transition) return TransitionSuperseded;
            return $state.transitionTo(redirect.to, redirect.toParams, redirect.options);
        },
        function() {
            return TransitionAborted;
        });
        return evt.retryPromise;
    }
    // Always retry once if the $stateNotFound was not prevented
    // (handles either redirect changed or state lazy-definition)
    to = redirect.to;
    toParams = redirect.toParams;
    options = redirect.options;
    toState = findState(to, options.relative);
    if (!isDefined(toState)) {
        if (options.relative) throw new Error("Could not resolve '" + to + "' from state '" + options.relative + "'");
        throw new Error("No such state '" + to + "'");
    }
}

@laurelnaiad
Copy link

Well I was thinking that if someone wanted to do something that wasn't a promise, then $q.when would allow for that -- so if you called the property retryCallback instead of retryPromise, and used $q.when to resolve it, then that's kinda what I meant.

@eahutchins
Copy link
Author

@stu-salsbury how would you handle the fall-through case where the handler was able to define the state? If I understand you correctly the retry path would always be recursive in your case (if my transition check actually works maybe that's fine).

@laurelnaiad
Copy link

I haven't fully grokked your code. In detour what I do is recurse on transitionTo after trying the promise, but I set a flag that I'm on retry so I don't retry a second time.

@laurelnaiad
Copy link

In detour I know that getLazyState returns a promise, so it's different.

      function transitionTo(to, toParams, updateLocation, secondTry) {
        var toState = statesTree.getState(to);
        if (!toState && loader.lazy.enabled && !secondTry) {
          return getLazyState(to).then(function() {
            return transitionTo(to, toParams, updateLocation, true); // true means second try
          });
        }
        else {
// etc.

@eahutchins
Copy link
Author

@stu-salsbury I added $q.when(evt.retry) added a private $retry parameter to the options to avoid infinite-looping, seems to work.

@eahutchins
Copy link
Author

@nateabele I'm not terribly happy with the redirect solution I came up with either, but it does have the advantage of being simple to use for the fall-through case and working with the deferred state definition support I just added. Can you see a way to get the same async semantics superseding the transition (maybe by bouncing through a loading state)?

We could move the redirect parameter last to keep a similar event API, but there is the confusing aspect that the input to is really different than toState which the other event handlers are called with:

$rootScope.$broadcast('$stateNotFound', to, toParams, from.self, fromParams, redirect);

this might be more confusing in the end.

@laurelnaiad
Copy link

Please don't take this as confrontational, but does anyone really need to redirect from a named state in this manner? Couldn't they just prevent the default and do whatever they want including go to some other state?

I figured this was just a sneaky way to get lazy loading in! :)

@eahutchins
Copy link
Author

@stu-salsbury I agree that the transitionTo() + preventDefault() technique works, but it makes it tricky to count how many reties have failed (for example). I'm primarily interested in lazy-loading, where the retry is key: you really just want one extra lookup and then fail, the way I did it makes this simple.

@timkindberg
Copy link
Contributor

Actually I just don't like calling it redirect, but now that I understand what that object is I think the approach is good. But its not a "redirect" in my opinion (that makes me think of the new state you want to go to), but really its the state that failed. I think unfoundState would be a better object name. I can then document the properties on the object in the docs.

$scope.$on('$stateNotFound', 
function(event, unfoundState, fromState, fromParams){ ... })

I'm good with it if we rename it.

@eahutchins
Copy link
Author

@timkindberg that's a much better name, I'll change it.

@timkindberg
Copy link
Contributor

I also think the properties on the unfoundState object should be 'name', 'params' and 'options'; not 'to', 'toParams' and 'options'.

@nateabele
Copy link
Contributor

One thing I wanted to mention earlier is not to get too hung up on the naming here. One of the things we have slated for later (probably around 0.4.0) is rewriting all state events to take one parameter, which will be an object representing the current transition, so whatever we do here is an interim solution that will ultimately be going away.

At that point, doing another $state.transitionTo() with $event.preventDefault() will work fine, since the transition object will keep track of whether a transition is pending or not. With that said, I don't see a reason not to just merge this and move onto other things.

nateabele added a commit that referenced this pull request Sep 16, 2013
Added a $stateNotFound event which can prevent or redirect state transitions.
@nateabele nateabele merged commit cbd8c02 into angular-ui:master Sep 16, 2013
@timkindberg
Copy link
Contributor

@eahutchins
Copy link
Author

@timkindberg the event handler can set a retry object in the event which gets wrapped in a $q.when(), and upon resolution will retry the transition exactly once if it hasn't been superseded. Not sure if we want to document that or leave it as a beta feature.

@timkindberg
Copy link
Contributor

@eahutchins I considered adding it but thought that we should just make a full FAQ section for lazy loading. Would you be able to write the FAQ on how to lazy load states? I was going to take a stab at it, but I don't want to mislead by writing it incorrectly. I've created the spot for it already.

https://github.com/angular-ui/ui-router/wiki/Frequently-Asked-Questions#how-to-lazy-load-states

@tregusti
Copy link

I would really like to see that FAQ updated by someone on the team. Ping @eahutchins =)

@timkindberg
Copy link
Contributor

As of now the best way to understand how this might work is to look at the src changes and tests over on the files changed tab above.

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

Successfully merging this pull request may close these issues.

6 participants