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

RFC: $transition$ injectable promise #1257

Closed
christopherthielen opened this issue Aug 7, 2014 · 20 comments
Closed

RFC: $transition$ injectable promise #1257

christopherthielen opened this issue Aug 7, 2014 · 20 comments
Milestone

Comments

@christopherthielen
Copy link
Contributor

I've been working on the "transition service" idea that's been tossed around here. See this commit in ui-router-extras: christopherthielen/ui-router-extras@92c4f81

So far, I have implemented and tested

  • $transition$: an injectable object which contains:
    • the to state/params
    • the from state/params
    • a promise which wraps the $state.transitionTo() promise
  • $transitionStart: a new event, fired in response to $stateChangeStart which exposes the $transition$ object/promise
  • $transitionSuccess and $transitionError Mirrors $stateChangeSuccess/Error, but are broadcasted based on the promise. I'm not sold on these, they seem perhaps unnecessary, but I put them in because it was ridiculously easy to do so.

$transition$

The $transition$ object contains the details of the current transition, to: { state: toState, params: toParams} and from: { state: fromState, params: fromParams } }. It also has promise which is resolved when the promise returned by $state.transitionTo() is resolved.

$transition is injectable as long as the transition is currently in process. This means it can be injected into onEnter/onExit/controller functions.

$transitionStart

The $transitionStart event fires in response to $stateChangeStart. This event exposes $transition to interested parties. Since $transitionStart exposes $transition$, it allows interested parties to listen for transitions, then process them using promises, which was impossible using $stateChangeStart.

benefits

  • Listen for state changes, respond using promises. When listening for $stateChangeStart/Success/Error, there is a separation between the starting event and the terminating event. One also has to listen for both Success and Error events to connect the dots.
  • Inject current transition into code invoked while the transition is pending. Currently, onEnter/Exit/controller don't know why they are being invoked. For example, the onEnter doesn't know if the destination state is that specific state, or some substate.
  • ??? other things?
  • Profit!!!

closes

I believe this change would address the following:

#1169
#1182
#1153
#1151
#1112
#1100
#1085
#1016
#575
#238
#23

@christopherthielen
Copy link
Contributor Author

Also #1259

@christopherthielen
Copy link
Contributor Author

As of version 0.0.10, UI-Router Extras now has the $transition$ api, as an incubator/early-access to what we're working towards in UI-Router.

See this plunkr: http://embed.plnkr.co/0jbkBmAJUoMDYpt8FXao/preview for a demo.

This demonstrates:

  • Listening for $transitionStart event
    • .then(success, error) on the $transition$.promise passed into the $transitionStart listener
    • .finally() on $transition$.promise passed into the $transitionStart listener
  • Injection of the currently active $transition$ into onEnter/onExit blocks of states
  • Injection of the currently active $transition$ into a controller.

@timkindberg
Copy link
Contributor

This is really awesome Chris! So would we have both $stateChangeStart and $transitionStart? I think we may just want one or the other. Are they different really?

@christopherthielen
Copy link
Contributor Author

The problem I ran into is that $stateChangeStart event has 5 parameters already, so to avoid a breaking change, I either had to introduce another event with just the event and a single transition param, or add transition as the sixth parameter. I figured $transitionStart would win in the long run, so opted for the first option since people would probably ignore the first 5 parameters if we chose the latter option.

@bvaughn
Copy link

bvaughn commented Aug 29, 2014

A key difference @timkindberg, at least in how it relates to the comment I left on 238, is the to state. The $stateChangeStart event doesn't specify a valid to-state and you end up having to manually track it.

@timkindberg
Copy link
Contributor

@christopherthielen I wouldn't be too worried about breaking changes if we are ultimately doing more good than harm. I'd rather keep the API simple and have breaking changes. Anyway, if you haven't noticed I'm not around too much anymore, but still loving what you guys are doing... so what I'm saying is its up to you!

@Gahen
Copy link

Gahen commented Oct 21, 2014

Hey! What's missing to make a PR out of this? Can I help?

@christopherthielen
Copy link
Contributor Author

@Gahen appreciate the offer! We've got most of this work already completed in the new $transitionProvider service in a branch for the upcoming 1.0.0 release. It's changed quite a bit from my RFC, but the major pieces are all there, along with a whole bunch of other goodness.

https://github.com/nateabele/ui-router/blob/new/src/transition.js

You can theoretically use my RFC version of $transition$ today with ui-router 0.2.11 by adding ui-router-extras 0.0.10 library to your project.

@Gahen
Copy link

Gahen commented Oct 21, 2014

Great! I tried the 0.0.10 ui-router-extras version against 0.2.11 ui-router. My main motivation was to have a sane and dependency injected $stateChangeSuccess, but I'm failing to achieve that last thing.

Can you show me how we would access the solved dependencies with this changes? This was talked about on #229, but perhaps changed on the way or I'm just not seeing where the solved depencies are.

On this example

$rootScope.$on('$transitionSuccess', function(event, $transition) {
     $transition.promise.then(function() {
         console.log(arguments);
     }); 
}); 

I tried inspecting both $transition and the result of the promise without any luck

Thanks!

@christopherthielen
Copy link
Contributor Author

The $transition$ from this RFC which is in ui-router-extras does not provide resolve information. It provides only what is listed on the top of this issue, primarily an injectable object which has a promise for the current transition. If I understand what you're attempting correctly, you could possibly do this:

$stateProvider.state('mystate', {
  url: "/mystate/:id",
  resolve: { 
    myResolve: function($http, $stateParams) { 
      return $http.get("/restapi/someresource/" + $stateParams.id);
    }
  }, onEnter: function(myResolve, myService, $transition$) {
    $transition$.promise.then(function() { 
      // Does not execute until the transition is successful.
      myService.heresTheResolvedValue(myResolve);
    } 
  }
});

@Gahen
Copy link

Gahen commented Oct 22, 2014

You are right, that is almost exactly my current approach, but it brings some problems as it won't trigger when navigating to a parent state, so I expected to solve them by using the ui-router-extras lib.

The solved dependencies get injected in the "new" branch of ui-router that you linked? As I would like to use it on a production site I may be more comfortable backporting that feature to 0.2.11 that using the unstable version. Or, perhaps more wisely, doing some quick implementation of similar functionality until 1.0 becomes stable.

@christopherthielen
Copy link
Contributor Author

I guess I don't understand. If you navigate to a parent state, the state you are interested in is exited, and its resolved values are gone. Can you help me understand the desired behaviour?

@Gahen
Copy link

Gahen commented Oct 22, 2014

Sorry, I wasn't clear, what I meant was that if I navigate from a child state to that state, the onEnter doesn't trigger (and that's OK because it's not entering, it was already on that state). So instead of using onEnter to execute a logic that should be run everytime that state is loaded (even if its from a child state of itself) I thought to migrate to $transition and execute it there, checking when I'm on that state.

I was expecting to do something like:

$rootScope.$on('$transitionSuccess', function(event, $transition) {
   // Lets suppose $transition.state.name === 'parentState'
   if ($transition.state.name === 'parentState') {
       if ($transition.state.resolve.data === 'someValue') {
          $state.go('parentState.childStateA')
       } else {
          $state.go('parentState.defaultChildState')
       }
   }
}); 

With the onEnter logic if I try to go back from 'childStateA' to its parent ('parentState'), the onEnter won't trigger and the state wont change to 'defaultChildState' nor 'childStateA', staying on the navigated one.

Hope I was clear this time! Thanks a lot.

@christopherthielen
Copy link
Contributor Author

Do you expect the state.resolve to be "re-fetched" when transitioning from a childstate back to the parent? Because that is not how resolves work. They are executed exactly once per time the state is entered.

However, if you understand that, and you still want to do the redirect, well it's not pretty, but you can do it with a little gymnastics with stock 0.2.11:

.config(function() {
  var myState = {
    name: 'mystate',
    resolve: { 
      myResolve: function($http, $stateParams) { 
        return $http.get("/restapi/someresource/" + $stateParams.id);
      }
    }, onEnter: function(myResolve) {
      myState.data.myData = myResolve;
    }, onExit: function(myResolve) {
      myState.data.myData = undefined;
    }, 
    data: { myData: undefined } 
  });
  $stateProvider.state(myState);
}

.run(function() { 
  $rootScope.$on($stateChangeSuccess, function (evt, toState) {
      if ($state.is('mystate')) {
        $state.go(toState.data.myData == 'someval' ? 'mystate.child1' : 'mystate.child2');
      }
  }
});

It's also worth mentioning that this sounds pretty similar to ui-router-extras' "deep state redirect"

@Gahen
Copy link

Gahen commented Oct 22, 2014

Hey! Thanks for your time.

The data approach should work, I just hoped to achieve that in a less hackish way, but would keep it as a backup solution; Deep State Redirect solves some of my use cases but fails on others as I don't always want to go back to the last child state, but to reprocess where to redirect analyzing how the dependency was solved for the current stateParams.

About dependency solving: as far as I can tell when the stateParams change the dependencies are re-processed. I tried to make a jsfiddle to prove it but the redirection part is hard to debug on jsfiddle, I'll make a standalone demo if you want.

@nateabele is completely insane to try your "new" branch of ui-router? I did a quick test and got some complains about $transition.init not being a function.

Thanks again, if you want we may continue this discussion in another issue more related to what was proposed on #229, perhaps if this RFC won't address that issue it may be reopened.

Edit: perhaps #228 is the right issue to be looking at.

@christopherthielen
Copy link
Contributor Author

the new branch doesn't route right now.

About dependency solving: as far as I can tell when the stateParams change the dependencies are re-processed. I tried to make a jsfiddle to prove it but the redirection part is hard to debug on jsfiddle, I'll make a standalone demo if you want.

What do you mean by this? Resolves are processed as states are entered. States are re-entered if their params have changed.

@Gahen
Copy link

Gahen commented Oct 23, 2014

Exactly that, I just mentioned it because it wasn't obvious to me from the beginning.

Well I guess I'll wait for any updates on this, let me know if you need early testing on this changes (transition + dependency exposing) or some help getting them done. I believe we should be able to access them them on the new transition events.

@DumboJet
Copy link

I am trying to prevent a transition on some occasions like that (and then use $state.go to re-initiate if needed):

$rootScope.$on("$transitionStart", function (event, $transition$) {
           if (isEditing()) {
                      event.preventDefault();
                      //.... Other stuff
           }
}

But event.preventDefault(); seems to do nothing and the transition DOES happen.
How can I achieve that?
Thanks in advance!

UPDATE:
I have changed your lib like so:

              var evt2 = $rootScope.$broadcast("$transitionStart", tData);
              if (evt2.defaultPrevented) evt.preventDefault();

..but then it still calls the callback of the $state.go(...).then() function. :(
Any idea how to avoid that too?

UPDATE 2:
OK. That was the best that I could find so far:

          // Decorate $state.transitionTo.
          $state.transitionTo = function (to, toParams, options) {
            // Create a deferred/promise which can be used earlier than UI-Router's transition promise.
            var deferred = $q.defer();
            // Place the promise in a transition data, and place it on the stack to be used in $stateChangeStart
            var tData = tDataStack[++transitionDepth] = {
              promise: deferred.promise
            };
            // placeholder restoreFn in case transitionTo doesn't reach $stateChangeStart (state not found, etc)
            restoreFnStack[transitionDepth] = function() { };
            // Invoke the real $state.transitionTo
            var tPromise = $state_transitionTo.apply($state, arguments);

              // insert our promise callbacks into the chain.
            if (tData.defaultPrevented) {
                throw '';
            }
            return tPromise.then(transitionSuccess(deferred, tData), transitionFailure(deferred, tData));
          };

          // This event is handled synchronously in transitionTo call stack
        $rootScope.$on("$stateChangeStart", function(evt, toState, toParams, fromState, fromParams) {
                var depth = transitionDepth;
                // To/From is now normalized by ui-router.  Add this information to the transition data object.
                var tData = angular.extend(tDataStack[depth], {
                    to: { state: toState, params: toParams },
                    from: { state: fromState, params: fromParams }
                });

                var restoreFn = decorateInjector(tData);
                restoreFnStack[depth] = restoreFn;
                var evt2 = $rootScope.$broadcast("$transitionStart", tData);
                if (evt2.defaultPrevented) {
                    evt.preventDefault();
                    tDataStack[depth].defaultPrevented = true;
                }
            }
        );

I throw an error to stop the transition.
Not good, but does the job...
But that prevents the $state.go(...).then() function from being called on successful transitions. :(
Please inform me if there is another way.

(Note: the code is from transition.js)

Thanks. :)

@christopherthielen
Copy link
Contributor Author

UI-Router 1.0 preview is available: https://github.com/angular-ui/ui-router/tree/feature-1.0

@christopherthielen christopherthielen added this to the 1.0.0-preview milestone Sep 30, 2015
@christopherthielen
Copy link
Contributor Author

This is ready to go in 1.0; closing

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

5 participants