-
Notifications
You must be signed in to change notification settings - Fork 3k
High-level method for state transitions #15
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
Comments
I like $state.go.
|
As stated in #14, the traditional way to build a state machine is by defining events that specify valid state transitions. Using proper definitions, it is possible to determine which transitions are valid for each state, and preventing the state machine from reaching an unstable or unexpected state. Here's the canonical car example: // Assume the following available states: "parked", "stalled", "idling", "firstGear", "secondGear"
.event("park", {
from: ["idiling", "firstGear"],
to: "parked"
})
.event("start" [
{ from: "stalled", to: "stalled" },
{ from: "parked", to: "idling" }
])
.event("idle" {
from: "firstGear",
to: "idling"
})
.event("shiftUp", [
{ from: "idiling", to: "firstGear" },
{ from: "firstGear", to: "secondGear" }
])
.event("shiftDown", [
{ from: "firstGear", to: "idling" },
{ from: "secondGear", to: "firstGear" }
])
.event("crash", {
from: "*",
to: "stalled"
]); The events then become methods available on // parked:
$state.start() // => "idiling"
// idling:
$state.shiftUp() // => "firstGear"
// firstGear:
$state.crash() // => "stalled"
// stalled:
$state.shiftUp() // => Error: Invalid state transition The syntax here is just an example, the important part is the structure and semantics. Another great feature of proper state machine events is that it makes wizard-like This also solves the 'can we get there from here?' problems with |
I think I love this! Should this a separate issue? Definitely needs to be explored! |
Having a separate issue is up to you. I just thought it made sense to put this here, since the events you define would be your high-level transition methods (and we can bundle parameter-passing up in them, too). The other cool thing here, obviously, is that it takes the complication of explicit state references out of your templates. |
Yeah ok, keep it here then. So would we then also do the hooks too like this?:
|
That could work, but you'd be more limited in how you apply them (for example, some hooks might apply over several events, so that could be pretty constraining). The syntax would at least need to be different though, because the second parameter needs to be able to be an array or an object hash. |
This is not a good example for the project: it is a traditional FSM example, whereras we are dealing with application state. I found in my production application that there was no need to define transitions between states: instead the enter/exit callback hierarchy handled everything perfectly with the exception of first restricting based on login. It is probably important to this that StateTree supports concurrent sub states, which are much more difficult to figure out how to map to routes. Defining transition events should be most useful for placing restrictions, so you might consider going back to #14 to discuss this. Either way you should make sure to use a real-world application routing example. |
@gregwebs are you saying the before, between and after are not good, or the $state.event idea? I'm not quite sure how we'd use the transition hooks, but I do see how $state.event could be very helpful for high level navigation and actions (as @nateabele pointed out it could be used for prev/next actions as one example). We've heard from at least @jeme on actions as well. |
I found the entire concept of event transitions ($state.event in this case) to be unnecessary. I just go to the state and the enter/exit callbacks handle things. I don't think I understand the next/prev suggestion. That implies a linear ordering of states, but even in a traditional FSM one jumps around based on conditions & events. It seems that under an event setup a user would have to make an extra effort to define a chain of states that work based just on a next() invocation. I think it could only be done automatically in our hierarchical setups as a way to move up & down from parent to child states when there is only one child or to move across sibling child states of a parent. |
If you had a couple of examples handy, that might help with a point/counterpoint. I'm not committed to one way or the other right now, but in my experience having events be a separate thing helps to keep hook points DRY.
This was just one off-the-cuff example explaining polymorphism of events and transitions, i.e. that the same event can be called in different contexts, with relevant contextual effects. In a linear wizard-like example, for one thing, it's possible to change your event definitions without having to poke through your UI bindings. Hope that makes sense. |
Yeah it wouldn't be a permanent API fixture, the next/prev events would be added by the user as needed. Similar to how the shift up/shift down events work in @nateabele's car example. |
I tend to agree with @gregwebs on this -- named transitions with actions associated with the transitions are obviously the bread and butter of traditional state machines, but I think those are quite different from our use case here. I think it would really help to have some real world use cases of what sort of things you'd actually want to do in those "when going from A to B, do this" hooks. The provisioning of the UI templates / controllers is taken care of separately already. I can't think of much myself -- even onEnter I don't see being used particularly often, and cases where i'd then additionally want to do different things based on what the previous state was seem rarer still. Happy to be convinced otherwise though. We're still talking about web application UI's here... The idea that the same "page" in the app would behave radically different depending on how you got there seems to imply a rather odd user experience (unless you're building some game where each state represents a "room" or something like that...) |
Ok @ksperling. It is an interesting idea and I think users would think it was really cool, but you are right that maybe it's not needed in a conventional website state management tool. If @gregwebs is saying we don't need it, then we probably don't (I mean he DID create a state tree library). So let's table that for now, @nateabele can write it up as a separate issue at this point and label as "review later" if he still feels strongly. Back to the main issue then... high level methods. You really have to now go back up to my first comment in order to get my initial feedback. https://github.com/angular-ui/router/issues/15#issuecomment-13609283 |
Some common scenarios of using state transition events:
I think transition should be chain of promises amendable for each step. e.g. : app.user.details.edit => app.admin.users.list chain:
just "pseudoapi": onTransition(from, to, transitionHandler, fallbackHandler)
onTransition(
'^', // '^' = parent,
'app.admin', // '*' = any, '-' = sibling
function(..injects..) {
return auth(); // promise
},
function(..injects..) { // executed after main transition rejection logic
$state.goTo('app.login');
}
) But this type of api doesn't solve some problems:
Btw ...onClick = function() {
$q.when()
.then(ui.overlay.show)
.then(function() { $state.go('admin.dashboard') } )
.then(null, function() { $state.go('login') })
.then(ui.overlay.hide, ui.overlay.hide) or $q.when()
.then(ui.overlay.show)
.then($state.go('admin.dashboard'))
.then(null, $state.go('login'))
.then(ui.overlay.hide, ui.overlay.hide) but where to place this logic ? Definitely not in controller, but $state.go inside state transition event handler looks also weird... |
I think the loading indicator could just be a We may be able to do state rejection in other ways see the issue related to that topic. Lets bring redirects into that discussion. Your 3rd bullet: interesting idea. Maybe add a The chain you specify us already happening behind the scenes but @ksperling would have to confirm that each ancestor dispatches its own event. There is another issue talking about dispatching. Perhaps we need more events to be dispatched; one per every exit and enter. No comment on the other stuff for now. |
That might actually turn out to be a rather annoying limitation. I choose to have a "view service" or "view provider" if you will... which basically allows to:
Controller being optional, but that means that views can be exchanged on the fly just as is possible with ng-include linked to a scope property. (The way it works actually gives the incremental rendering of views as requested above, not something I particular aimed for though) |
@jeme One problem I have with the 'step by step' idea is that the intermediate steps are not necessarily valid states in the state machine sense. In the sample app, going from "about" to "contacts.detail" would pass through "contacts", but that is an abstract state that cannot be meaningfully navigated to by itself (the whole set of exit/enter callbacks is still invoked though, but only after all the asynchronous resolves have completed). There are multiple useful aspects to doing the state transition atomically:
I guess the incremental loading may be desirable in some cases, even though I'm not sure this really needs to be done at the state machine level -- if its the exception rather than the rule in your application you can always do some additional async work in the controller after the view has been rendered. It might be feasible to implement your 'safe state' idea -- even though I'd probably call them 'eager' rather than safe -- it seems like error handling on the application level would become rather tricky though. I think the simplicity and robustness of atomic transitions probably makes them the right choice in 98% of cases, so to me incremental transitions would be in the "investigate for v2" category at the moment. The idea of a separately usable $view service is interesting; the interface between ui-view and $stateProvider is currently $state.$current.locals, which is a map from view names to views (template/controller/resolved deps), so rather than just setting that directly we could think about exposing that touch point as a service. We have to think carefully about how this would work in terms of asynchronous loading -- $stateProvider only "pushes" a view into this global data structure when everything is fully resolved, wheras your $view.set(...) example still requires things to be loaded before the view can actually be 'set', so maybe should be called $view.load(...) instead. We'd need to be careful to make sure that overlapping calls to $view.load() have a sane outcome (last call wins i guess), and how this would interact with concurrent synchronous $view.set() calls as they would be done by $stateProvider. The behaviour when you manually update a view that $stateProvider also manages could also be problematic. Another question is how clearing views would work -- $stateProvider currently simply pushes all views assigned by the current state in one atomic operation (by replacing $state.$current), which implicitly also clears any views not assigned in the current state. If views can be set via $view directly, it seems on a transition A -> B $stateProvider would need to work out and explicitly clear any views assigned by A that aren't assigned in B. I can think of some vague use cases for $view, e.g. having some sort of contextual assistant view in a side bar that is set dynamically based on user input or some other conditions. Do you have any concrete use cases for this feature in your apps? Maybe we should open a separate ticket for $view and work out how it would work in detail, and see if there are enough use cases to justify the added complexity (which hopefully won't be so big). One thing I do quite like about $view would be that it allows people who don't like $state for some reason to implement their own state machine while reusing $urlRouter and $view, but this is a somewhat niche feature as we should aim for $stateProvider to cater to the majority of use cases. |
@ksperling It wasn't me who brought the step-by-step up, it was mostly a desire i gathered from @ludinov's response...
Thats not the only thing it does to me, it also separates responsibilities/concern, and even if it is not for the outside to use, that would certainly be beneficial from an inside perspective as well... Adding the necessary details to support what you refer to as atomic would properly also be possible, since all my current state transitions is by very definition atomic, I don't have the issue you mention, the reason I get incremental reload on certain occasions is because I in my usage of the implementation perform multiple transitions. |
@ksperling I was gonna start working on parameter inheritance for |
Go ahead, I'm still working on the 'resolve' stuff. Maybe we should just include it in transitionTo directly at this point -- the idea of a wrapper was that somebody could decorate $state to modify it's behaviour, and still take advantage of sugar layed on via wrapper functions, but I don't think that sort of use is common (or happening at all?) at the moment. |
@ksperling Yeah, not really. Generally, I doubt states will be nested deeply enough to make the idea of i.e. |
Yes, third parameters should become an options hash, which then internally becomes the "transition" object that gets passed to onEnter/onExit etc instead of the current separate to/from/toParams/fromParams etc. |
Being able to say ".." is less about saving typing and more about decoupling states from their parent where that makes sense. |
Okay, sounds good. I'll get started on it, but you'll have to show me how you want it. Re: "..", that makes sense. Should that be baked into |
Maybe add an optional 'base' parameter to findState() so that if the first parameter is given as a name and base is specified it resolves the shortcuts. We have to think about what we want the syntax to be... I think we should avoid "/", because it would make stuff look too much like URLs, which will be quite confusing because the state nesting is often similar to but different from the URLs. Maybe ".." for parent "..." for grandparent, and "&" for current state? All being treated as prefixes, so you can say "&child", "..sibling", "...uncle" It's also worth thinking about if we want these to effectively be string operations, or how we handle the case where a state specifies it's parent explicitly rather than via the dot syntax. I think it makes sense to resolve the up references by walking the state.parent tree, and then treat a suffix (if present) by appending |
(I think we should allow any number of "." for up navigation, even though in practice people probably won't use more than 3 or 4 at most) |
As an aside, it seems to me that while the "parent.child" way of naming states and implicitly setting the parent is nice when there is tight coupling between the child and the parent anyway, but that the explicit parent setting is nicer when they are essentially independent, as it makes it easy to move the child to a different place in the hierarchy / navigation structure of the app without having to rename any states. The ability to move states around by having them only loosely coupled to their parent is also something that needs to be considered in any changes to the view naming approach. |
Commenting to get notifications |
@mgonto Alternatively (to spare notifications for future dev teams), there's a menu at the bottom. ;-) |
Ohhh! I've never seen it before!. Sorry for the notification. Next time I'll use the button. It should be at a more visible place. Thanks! |
May be too late, but I was thinking it may be nicer and easier to read if we use |
I like "^". A separate symbol for sibling doesn't seem necessary though; wouldn't that just be "^sibling"? or maybe "^.sibling" I kind of like the look of the latter. "^^.uncle" seems alright too, more obvious than "..uncle" I'd say. The only drawback is that "^" from the regex analogy (and how we want to use it for URL patterns) anchors at the very top, not just one step up. |
Any update on when this might be landing? I could help out if needed. |
I'm actually pretty close to done with a pretty big refactor which includes these and other enhancements, and I'll be in the air with no internet for 16 hours starting tomorrow afternoon. By the time I land, UI Router will be a whole new library. ;-) |
Looking forward to seeing your changes!!! |
I'd say this is done. @nateabele? |
Yup. |
transitionTo() is a fairly low-level method in that it expects a "fully qualified" state and all parameters to be passed in. I want to add a higher-level method on top of it for everyday use where you can do stuff like
The text was updated successfully, but these errors were encountered: