-
Notifications
You must be signed in to change notification settings - Fork 3k
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: Parallel states - Proof of Concept code #894
Comments
Just real quick... one of the coolest fucking things I've seen created with ui-router yet. I am having trouble figuring out what features are coming from your modifications to the src and what features are coming from the boilerplate in your script.js plunkr file. Like, what could you do without all the extra helper functions everywhere? Or are those required? Obviously this is just an RFC, I'm just trying to get a feel for what is in included and what isn't. So looking at it more. Seems like the parallel aspect is included in the src changes, so we get:
The boilerplate gets us:
I wonder if instead of using a In root.tabs template: ...
template: ' <div ng-show="isTabVisible(\'root.tabs.S1\')"><div id="root_tabs_ui-view_S1" ui-view="S1">Nothing Loaded</div></div>' +
... Then in root.tabs.S1: $stateProvider.state('root.tabs.S1', {
controller: timerCtrl,
template: 'whatever',
scope: true,
parallel: true,
parallelView: 'S1',
url: '/s1'
}); I'd have more feedback but I'm curious what @nateabele, @MrOrz, and @empaempa think about it. |
Thanks for looking at this, and I'm glad you think it's cool. That hopefully means I'm on the right track. Your analysis is correct. The modifications to ui-router allow parallel states that can be bound to ui-views. The module code implements the tabs state management and UI (i.e., it uses the parallel state support from the ui-router modification to implement parallel tabs). I took a hint from some previous comments on the issue tracker that "tabs" themselves are outside the scope of ui-router, so I intentionally kept all the "tabby" stuff in my module. The controller for "root.tabs" contains most of the tabby logic and could (probably should) easily be converted to a directive. I have put about 12 hours into this so far, attempting to attack the problem from 4 or 5 different angles, so I wanted to make sure I'm on the right track before dumping more time into improvements. Edit: Ooops, I misinterpreted your comment and made a huge reply based on my misinterpretation. I'm leaving this up for posterity Regarding the markup convention I implemented, I tried a number of different mechanisms before settling on Here are some thoughts: Parallel states must be direct substates of the parent state. Your comment about the parallel-state attribute can be deconstructed to: How do you (a) declare the parallel states and (b) bind those states to ui-views from the parent view. In my example, I don't like that the ui-view in my proof-of-concept is explicitly referencing a child state as opposed to a view, which I think is your objection as well. My rationalization for referencing the state as ".S1" is that a raw If we ran with your suggestion,
Either way, I am definitely trying to link the ui-view to a specific substate, whether that is direct by referencing the substate name, or via a parallelView name (unique among all the child states) found by checking all parallel child states. Food for thought, here are some of my failed approaches: Finally, I decided to try to embrace the currently built-in "raw" ui-view mechanism. I noticed that multiple |
Tim, I just realized I completely misunderstood the point of your suggestion. I'm leaving my last comment up for posterity, maybe there's something discussion worthy in it.
If I had read your comment more carefully I would have noticed the most important part. Is there a precedent for child states going "up the chain"? The way I currently think of views and states is that the parent state chooses a view whose template is responsible for plugging in the child, via ui-view placement. |
I converted the tab handler to directives and changed the ui-view syntax to The relevant markup now looks like: // The tab list state
$stateProvider.state('root.tabs', {
controller: function ($scope, $state, $timeout) {
timerCtrl($scope, $state, $timeout);
$scope.isStateActive = function (statename) {
return $state.includes(statename);
}
},
template: '<b>root.tabs</b>: Started {{delta}} seconds ago' +
'<input ng-model="data" type="text">{{data}}' +
'<br><input type="checkbox" ng-model="showInactiveTabs">Show inactive tabs' +
'<ul class="tabs" parallel-state-controls>' +
' <li ng-class="{ active: isStateActive(\'root.tabs.S1\') }" parallel-state-selector=".S1">S1 (Parallel State 1)</li>' +
' <li ng-class="{ active: isStateActive(\'root.tabs.S2\') }" parallel-state-selector=".S2">S2 (Parallel State 2)</span>' +
'</ul>' +
// Here is where the parallel states are bound to the UI. I'm using the .SUBSTATE nomenclature to
// mark a ui-view as following a parallel state tree.
// Note: Wrap the ui-view in a div because ng-show doesn't seem to work on a ui-view
' <div ng-show="showInactiveTabs || isStateActive(\'root.tabs.S1\')"><div id="root_tabs_ui-view_S1" ui-view=".S1">Nothing Loaded</div></div>' +
' <div ng-show="showInactiveTabs || isStateActive(\'root.tabs.S2\')"><div id="root_tabs_ui-view_S2" ui-view=".S2">Nothing Loaded</div></div>' +
'',
url: 'tabs'
}); With all the fluff removed, here is the bare bones markup: <ul parallel-state-controls>
<li parallel-state-selector=".S1">S1 (Parallel State 1)</li>
<li parallel-state-selector=".S2">S2 (Parallel State 2)</li>
</ul>
<div ui-view=".S1">Nothing Loaded</div>
<div ui-view=".S2">Nothing Loaded</div> |
Ha I love when someone is so excited they write about 2000 words and 2 directives before I even get back. So let me start with a disclaimer. UI-Router is in big flux right now with the bower issue, @nateabele has about 100 local refactors he has yet to commit, and several medium sized features that are still in the 'baking' process. With all of that, this is probably not on a high priority list (mainly because its large size), but its really cool and its something I know devs have wanted multiple times, so its a good thing to explore (and you seem to have the smarts to explore it properly). Basically the less intrusive that the changes are in the source, and the less conceptual overhead it adds to ui-router, means the more likely it will get in quickly. @nateabele and I are really stubborn on API elegance (esp since there's already several concepts in ui-router that are not easy to grasp at first blush). Anyway, if that's all good with you we can continue to explore this. Ok so your question from two comments up first:
Yes I think this is how it currently works. A parent state template can define multiple named views as such: <div ui-view='a'/>
<div ui-view='b'/>
<div ui-view='c'/> But its the child state that decides how to fill those shells: $stateProvider.state('child', {
views: {
'a': ...,
'b': ...,
'c': ...,
}
}) To me, this is going 'upwards'. Now for your comment above (with the new directives and markup). This does feel a lot better, perhaps we'd roll all the directives into the module. I'm not fully liking the <ul parallel-state-controls>
<li parallel-state-selector=".S1">S1 (Parallel State 1)</li>
<li parallel-state-selector=".S2">S2 (Parallel State 2)</li>
</ul>
<div ui-parallel-view="S1">Nothing Loaded</div>
<div ui-parallel-view="S2">Nothing Loaded</div> I removed the dot before the state names, but I'm not sure if that's good. They do help show that its children states when the dot is there. But then I think it may also make ppl feel as though they can use any relative or absolute state targeting string there (e.g. '^', '.child.grandchild', '^.sibling'); could they? Would they? I think we need more brains in here. |
Aah I see. I haven't used multiple named views, so I misunderstood what was going on with them. You don't like ".S1", but is it primarily because of the potential user confusion on what they can put there? Of course those cases could be guarded during compile time, i.e. if (name.indexOf(".", 1) != -1 || name.indexOf("^") != -1)
throw new Error("ui-view='" + name + "' illegal substate reference. You can only reference direct child substates. "); That said, the more I consider your idea, the more I agree it makes sense. <ul parallel-state-controls>
<li parallel-state-selector=".S1">S1 (Parallel State 1)</li>
<li parallel-state-selector=".S2">S2 (Parallel State 2)</li>
</ul>
<div ui-parallel-view="S1">Nothing Loaded</div>
<div ui-parallel-view="S2">Nothing Loaded</div> Is this more along the lines of what you're thinking? var substate1 = {
url: ..., name: ...,
views: {
S1: { template: '', controller: function() {} }
}
}
var substate2 = {
url: ..., name: ...,
views: {
S2: { template: '', controller: function() {} }
}
} |
Yes it is. I'm still undecided if the substate should have to use the I'll have to think more about this. Meanwhile hopefully some more people chime in before we go to far in any one direction. |
Updated plunkr: http://plnkr.co/edit/YhQyPV?p=preview I've made some more progress on this:
I need this feature for a project I'd like to port to ui-router, so I'm going to continue plugging away (or should I say "plunk'ing away"?). Hopefully I'll get more feedback from the other players. Tim, I took your disclaimer to heart:
I revamped the way nested parallel states plug into their parent ui-view. I went with your suggestion about the parallel state having a named view which plugs into the matching named ui-view in the parent template. Now that I understand how regular named views work, this makes sense and seems consistent. It also simplified/removed a bunch of the code I was kludging before. By extracting most of the code to a $parallelState, the integration points become simple and apparent. Have a look at my fork again. There is a lot of added code (plunkr code and parallelState.js), but the important conceptual changes are found in state.js and viewDirective.js. The following 5 integration points are confined to three functions: registerState(), transitionTo(), updateView(). Check the diffs ( https://github.com/christopherthielen/ui-router/compare ) for exactly what changed in state.js and viewDirective.js. The 5 integration points are as follows: 1) during registerState, if the state is defined as parallel, then register it with $parallelState function registerState(state) { ...
if (state.parallel) $parallelStateProvider.registerParallelState(state); 2) Reactivate parallel stateNormally, during transitionTo we check for the states that are "kept" and reuse those locals. Any non-kept additional substates are resolved by calling resolveState(). When those states are resolved, then the transition occurrs. Instead, we now first check if we are transitioning back to a previously inactivated parallel state. If so, restore locals from $parallelState instead of calling resolveState(). Make a note that we did this by adding the state name to a local variable "restoredStates". We'll use that after the transition occurs to notify the states via onEnter/onReactivate var restoredStates = {}; // Used after all states are resolved to notify $parallelState
for (var l=keep; l<toPath.length; l++, state=toPath[l]) {
var restoredState = $parallelState.getInactivatedState(state, toParams);
locals = toLocals[l] = (restoredState ? restoredState.locals : inherit(locals));
if (restoredState) {
restoredStates[state.name] = true;
} else {
resolved = resolveState(state, toParams, state === to, resolved, locals);
}
} 3) Register "exited" (inactivated) parallel states as inactivatedAfter transitioning states, we normally locate exited states and execute their callback "onExit". Instead, check if we are "exiting" a parallel state that should continue to live as an inactivated parallel state. Instead of exiting the state and removing its locals, register the state as Inactivated via $parallelState, then execute the callback onInactivate (as opposed to onExit). Notify $parallelState of any states that were actually exited, so we can check if this orphans any descendant inactive parallel states. // Check if we are transitioning to a state in a different parallel state tree
// fromPath[keep] will be the root of the parallel tree being exited
var parallel = keep < fromPath.length && fromPath[keep].self.parallel;
// Exit 'from' states not kept
for (l=fromPath.length-1; l>=keep; l--) {
exiting = fromPath[l];
if (parallel) {
$parallelState.stateInactivated(exiting);
} else {
$parallelState.stateExiting(exiting);
if (exiting.self.onExit) {
$injector.invoke(exiting.self.onExit, exiting.self, exiting.locals.globals);
}
exiting.locals = null;
}
} 4) Reactivate previously inactivated parallel state(s)After transitioning state, we normally invoke the onEnter callback for any added states. Instead, we first check if the newly added states were re-activated (via integration point #2, above). If so, notify the parallel state it has been reactivated, and remove it from the inactive parallel state registry // Enter 'to' states not kept
for (l=keep; l<toPath.length; l++) {
entering = toPath[l];
entering.locals = toLocals[l];
if (restoredStates[entering.self.name]) {
$parallelState.stateReactivated(entering);
} else if (entering.self.onEnter) {
$injector.invoke(entering.self.onEnter, entering.self, entering.locals.globals);
}
} 5) Short circuit ui-view's updateView()Normally, after a successful state transition, all ui-view(s) will update themselves. The ui-view might re-initialize itself (and thus, all its sub-views) if the state mapped to it has changed. Instead, check if the state transition is changing from this parallel subtree (or "universe") to another peer parallel subtree. In other words, the state was on a substate of one parallel tab, then the state transitioned to another substate of a parallel tab, a peer tab to the first. If so, then short circuit the ui-view's updateView. This leaves the DOM intact and doesn't rebuild the view subtree. var eventHook = function (evt, toState, toParams) {
if (viewIsUpdating || $parallelState.isChangeInParallelUniverse(view, evt, toState)) {
return;
} |
Wow man! Just amazing. People are really going to like this and I really appreciate you working on this so thoroughly. This really epitomizes "contributing". I know you've already put so much effort in and honestly it is usable the way it is, and not overly confusing (which is good for a new concept like parallel states), but I did have some more thoughts, but don't feel like you have to entertain every little whim that comes to my mind, but I'll say them anyway. The two new directives (parallel-state-controls and parallel-state-selector) feel a bit odd to me. Now maybe this is because you felt you needed to keep them as separate from the src changes, but I'm really rethinking that notion. Now if we keep them that's fine—that was actually another side question I had: were you thinking that these directives would be part of ui-router core? Anyway, these two directives really just do a couple things (from a end users perspective); 1) parallel-state-controls allows you to do a 'deep' reactivation and 2) parallel-state-selector just activates a parallel state. I was thinking instead of parallel-state-controls, we could just have another state config option called Additionally, is there a reason why we couldn't just use ui-sref instead of parallel-state-selector? I'm fine with baking all of this functionality into ui-router. I think others will be too, screw whatever has been said before. This is a slightly newer spin on the concept that I believe works with core. Now this is all naive to whether this is technically possible, so maybe its not doable. Some other thoughts:
|
Tim, I made some changes, updated the plunkr, and committed src changes to my fork. Baked in deep tab state re-activationI implemented your suggestion about configured state deep re-activation and nuked both tab control directives. This feels much cleaner! I don't think I like config option's name however, state.js integration rewriteThe 5 integration points remain the same, however I rewrote the code to hopefully be easier to comprehend. I am now returning a parallelStateTransitionType object I created a function to return the type of "re-entering inactive state" transition is occurring. It indicates if we are re-activating a state with the previous locals, or if we need to re-initialize because of updated state params. This is due to a bug I found where a parallel state transition to the same parallel state with different params wasn't treated properly. (I've added a state param to root.tabs.deep and controls in root.tabs.deep.deep.nest in my example which demonstrates the fix)
And a nice way to determine if we should onExit or onInactivate:
And a nice way to determine if we should onEnter or onReactivate:
questions
Yes, I see no reason it wouldn't work currently but I haven't tried it.
Nothing to see here, move along. (copy/paste stupidity) state transition thoughtsI have been thinking about controllers that want to know when they're being transitioned away from. Currently, a controller can intercept stateChangeStart and check if they have dirty data that needs to be saved, etc. With state inactivation, this concept no longer makes sense. We need to add a way to check if the controller is about to be inactivated, as opposed to about to be exited. |
So. Fucking. Awesome. Its so conceptually easy to pick up now, I can't believe you've done all of this man. I really still want @nateabele to take a look, I think he's a bit busy lately (aren't we all). I was wondering if instead of setting I'm really surprised others haven't chimed in yet either. |
But then we are adding complexity. I'm thinking of a comment by @nateabele in issue 618... are those suggestions a "10-lines-of-user-code" scenario?
|
I think persisting can come later, we don't want to do too much until there is a valid outcry for it. I think both 'parallel' and 'retain' make sense to me. 'retain' may be a bit easier conceptually—"I want my stuff to stick around". Some people call them "sticky views" as well. So something like "sticky" could also work. "parallel" is certainly more technical sounding. We still need @nateabele to take a look too. |
Hi all, I've kept an eye on the thread for a while but not until now do I have some spare time trying this out. First Try on Modal DialogsThe first thing I tried is to use @christopherthielen's branch to recreate the modal dialog scenario mentioned in #562 . It is achievable by adding a root state on top of the detail state and the index state. Follow UpHowever, when I extend the example above to a more realistic situation, problems start to emerge. In this example I added multiple states, most of them are listing items, which are commonly seen in e-commerce websites. Since they all opens the item detail in modal dialogs, all of them requires Similarly, my proposal #562 suffers from the complexity in state configuration: the Intuition When Dealing With UIOn the other hand, what makes me excited is that Chris' initial proposal shed light on a whole new direction: specifying the view behavior in template, instead of in state configuration. I think it's more intuitive to add html attributes to I suddenly realized that what I really need back in #562 should be a boolean attribute like I think the boolean attribute should also work in the minimal example in Chris' plunker, but I have not yet tested with code. Is there point I am missing above? I'd like to hear from you. |
Jeez, did I really not respond to this until now? Sorry about that. I have a more extended reply once I'm able to digest all of this. In the meantime I will say, the code looks great, but a lot of what this is touching is about to change significantly. |
@MrOrz thank you for taking the time to look at this and offer your thoughts. @nateabele I look forward to your thoughts. How in-flux are we talking here? Is it worth attacking other issues at this point? I have been working 774 (animation and anchor tag oddities), but don't want to waste effort. thoughts on your Follow Up commentsFor the "modal" use case, my thought was to create a standard top-level modal state, then a top-level parallel state with substates for "everything else". I updated your 'Follow Up' plunk here: http://plnkr.co/edit/i0sd6j (note, I had to fixa bug in Instead of each "products" state marking itself as parallel, you can mark a parent state as parallel. I suppose that parent state could even be marked abstract too (but I didn't do that in the plunk). instead of:
I sructured your states as follows, and got some reusability:
parallel declaration on state vs template/viewI think there is a lot of room for discussion here and many ways we could approach this. We need to consider multiple facets, including but not limited to: What is most intuitive? What is most consistent with the current API? What approach offers the cleanest implementation? I agree with you that it feels initially the most intuitive to mark the ui-view in the template as parallel/retain, which explains why I approached my first implementation attempt that way. I suspect, however, that there are there cases where a state tree should absolutely know that it has to support "retain". My use case involves tabs that stick around for a long time. Those tabs have user-input dirty-checking and warn the user if they're about to lose their changes. In this case, is it important to be explicit about parallel parents, and for the states to "know" they have a parent parallel state, or is having a parallel view as some ancestor enough? What I don't like is subtle behavior change when some substate of a state tree is (or is not) placed inside a parallel I don't feel super strongly about this, but I feel that configuration at the state level smells slightly better to me. Edit: I should clarify, in my dirty checking example, the controllers mapped by the state should only warn the user about dirty input if the state is about to onExit, not onInactivate. |
I initiated #562 under a specific use case of my own website. All pages in this website would open a drawing or an article in a modal window. That's why my proposal wanted to alter views' behavior, instead of states'. Chris' implementation, on the other hand, provides a more comprehensive solution to the problem. By manipulating states directly, locals and context can be stored and retrieved and even deep state re-activation can be achieved beautifully. Although such functionality is not required in my use case, Tim's implementation surely outperform mine for its flexibility. It took me some time to understand the concept though, because my thoughts were too focused on how a specific "view" should behave (that's probably why I came up with the new |
Yeah I may like the "sticky" or "retain" concept better than "parallel". @christopherthielen is it built in to allow multiple parallel states that all show at the same time; aka that don't use the 'tabbing' 'only show one at a time' feature? |
It is. In my plunkr, you can see this by checking "Show inactive tabs" |
Ah ok so that is the default. Its up to the dev to show/hide the appropriate views if they so choose. |
http://plnkr.co/edit/joqCreywFtnUCFMzVzBF?p=preview This is an implementation of pinterest-style routing on top of your parallel change. This allows you to refresh with a dialog open, and then arrive at the full sized display of what was previously in the modal. Without something like this, you end up being forced to link/route directly into a modal (or other similar state), which is a little strange, especially when the route doesn't encode any information about what's supposed to be behind the modal. Edit: this also resolves #317 I think. |
@ashaffer so you like it then? |
@timkindberg yes, very much. |
@ashaffer cool 👍 |
@ashaffer: Just FYI, I believe that implementation will incorrectly pop up a modal with:
|
@braco Good point. I think the resolution is that instead of defaulting to modal display when there's a prior state, you have to explicitly describe which prior states cause the modal to open. This has the slight quirk that if in the 'click anything' step, you click on one of the states in the modal list, then pressing back will take you into a modal (when you had been at the full page description). For my purposes that quirk is acceptable, but it may not be in all cases. |
I don't mean to spam everybody watching this, but I just wanted to say that this would be a great improvement to an already awesome project! I opened a similar issue a couple of days ago and @timkindberg pointed me in the right direction. Probably there are a lot more devs out there who would take advantage of this. I just finished reading through the whole PR and think you guys are coming up with something very nice! |
Last night I woke up from a dream, thought "maybe deepStateRedirect is just a special form of abstract: true", then fell back asleep.
|
Hmm I understand the logic but seems a bit weird to mix those concepts. |
I was thinking for tabs, we don't really ever want to activate "the tab" itself, rather we want to activate a substate. It seemed to be a specialized version of the "abstract state with default child state" concept from #27 or #948 (comment) |
brb, reading academic papers about finite automata. |
Here's my two cents from a math perspective: All of these proposed changes are adding layers of specialized behavior. What would be more ideal would be a conceptual rebuilding that allows for arbitrary state-network topologies. Here's how I would do it. Here's a small outline of how this proceeds, because it's difficult to build up linearly and might be a bit confusing on first read:
States (
|
Brilliant! I was just getting into FSMs and was thinking state manipulation should be like a graph. Btw the state data and state parameters give way to a state history that stores more than just the state names but transient state information hence the ability to have widgets that remember themselves. Furthermore I would suggest to make state transitions as promises, so they can be processed asynchronously. Another thing is guards, so you can move any transition logic out of the controllers but into bootstrapping phase. This means things like preloading resources before a state is transitioned to and authentication logic, such as saying that the user must have an authenticated session or authorised permissions to be allowed to change state. Then the logging of where the user went and how they used the GUI becomes easier which kind of incorporates AOP. Lastly any state transition should be hooked up to an eventbus, which allows you to listen for state transitions and potentially trigger state transitions, thus creating an event driven fsm. |
@DylanLukes You are my new favorite person. This is a fantastic write-up, and parts of it track very closely with my thinking on this. Some of the above, however, doesn't quite square with ui-router's architecture. Two examples: (1) states have a 1:n relationship with views, where n >= 0, and (2) the design intentionally eschews the rigor of an FSM, and though we lose some benefits by doing so, I think we can regain them by coming at it the problems it solves from a slightly different angle. In any case, I very much look forward to your proof of concept. @christopherthielen The above write-up represents parts of why I've been waffling on this for so long. There are many parts of ui-router's design that are deeply, fundamentally wrong, and it's taken a great deal of time and pain to rectify them. In fact, I am currently in the process of finalizing my second attempt at a full rewrite of ui-router's internal architecture. None of the parallel-state proposals currently on the table feel completely right, and I'm pretty wary of repeating the mistakes of the past. On the other hand, none of what we're facing are new problems. I'm fairly confident that there is One True Way™ here, and it probably exists in an academic paper from 10+ years ago. Regarding the rewrite, one of the design goals was to make transitions fully atomic, such that failure could safely occur at any point in the transition process without leaving the application in an inconsistent state. Considering the above, I believe this will be a useful primitive to build off of once we are ready for a final parallel-state implementation. It's not ready for prime-time, but parts of the API very closely mirror the above: $transitionProvider.on({ from: "home", to: "somewhere.else" }, function($transition$, $http) {
// ...
});
$transitionProvider.onEnter({ from: "home", to: "somewhere.else" }, function($transition$, $http) {
// ...
});
$transitionProvider.onExit({ from: "home", to: "somewhere.else" }, function($transition$, $http) {
// ...
});
$transitionProvider.onSuccess({ from: "home", to: "somewhere.else" }, function($transition$, $http) {
// ...
});
$transitionProvider.onError({ from: "home", to: "somewhere.else" }, function($transition$, $http) {
// ...
}); Anyway, hopefully that makes things a little bit more clear about where we're headed. Sorry for not being better about feedback up till now.
@CMCDragonkai I don't follow. State transitions already are promises. |
Yep I know. Just for with regards to Dylan's ideas. |
A few more thoughts (skip to the bottom for the more interesting ones):
Going from those visualizations, an FSM seems like the best initial choice. However, for maximum flexibility, we want to be able to name the transitions, and have a dynamically generated set of transitions. This entirely precludes an actual FSM structure. So, also, I'm not sure Promises are actually the right mechanism here. I've been thinking about how to represent traversals and, of course, my first thought was a chain of promises. However, you end up needing the resolved values of those promises later, so they basically just act like lazy thunks. Moreover, you can't roll back a stateful promise. So... here's some thoughts on managing state: Traversals as ScopeThis is actually really intuitive. The traversal chain is a stack. On just about any computer, the currently active program only gets at its state via whatever it has in scope. So, let's treat a traversal as a sort of dynamic scope (angular provides a lexical/static scope). As for implementation, I think the traversal chain should have a global object with each field mapped to a sparse "stack" tracking the history of values in scope. This enables roll-backs as well. The only problem is that this basically means we can't allow any mutable values in scope state at all. They can't be rolled-back, and will pose some serious issues. We can get around this by keeping "references" or "tokens" to be used with other services, but those services themselves are under no obligation to comply. So, as it stands I'm actively thinking about the state issue here and a sane, but functional (read: practical, pun intended) way of managing it all... Edit: We also need a way of having "sticky" states that don't lose their state when rolled back... I think these are best modeled by states which have back or self edges. So, we can "go back" without rolling them back. The implication, theoretically, is that to "go back" through or from any sticky state to another state, every state along the way must be sticky (there must be back-edges all the way). Food for thought. |
Some further stuff that's concerning me:
Edit: dynamic behavior could possibly be embedded in the traversal chain via scoped values, assuming these are available to future traversals. Then the behavior is dynamic, but constrained to being a result of previously scoped values (user input categorically falls within the purview of the active state). So, maybe only the tip of the traversal chain actually needs side-effecting behavior :). |
Just finalized the core data structure for my proof-of-concept. It's basically a generic graph with the following features:
I've decided to go with DAG's for the routing topologies as they're a good compromise between fully dynamic states/transitions and strict hierarchical routing. As my code is written in TypeScript compiled to ES6, that'll be a prereq, though it all works fine with es6-shim so far. Oh, and it plays nice with D3 :). Some example "exotic" routing topologies: |
@christopherthielen and anyone else using your parallel state implementation: There is a small bug in your patch. When you define parallelSupport, you do so in the provider which uses the provider's injector to invoke onEnter/onExit hooks. This causes you to be unable to inject services in the normal way in these hooks. |
@ashaffer thanks! I moved parallelSupport inside $get and injected the $injector there. |
@christopherthielen I just started following this so forgive me. Based on your demo it looks like your back button history states simply follow the users click sequence. Is there a plan to adapt this so that each tab or parallel state gets its own history stack? It sounded like @DylanLukes was sort of describing this but I wasn't sure. It seems like it would be useful if there was an option so that a back button wouldn't jump you between different tabs but rather would just back off the history for the particular state you have currently active. I am curious if this is a planned feature or would I have to modify it to do this? |
I've modified @christopherthielen's code to invert the meaning of parallel in a separate branch. If anyone else needs it, it's available here: https://github.com/weo-edu/ui-router/tree/issue-894-inverted-parallel |
@ashaffer I'm curious as to how you're defining your states, with the "inverse" paradigm. Do you have one modal state marked as parallel? |
@christopherthielen Ya, I just stick parallel: true on my modal states. |
Hey all, Since it looks like this implementation of "parallel states" isn't going to make it into mainline UI-Router, I have refactored my code into a separate project that plugs into the stock UI-Router (using a few hacks like hijacking the to/from paths). For anyone using my fork to implement parallel states, I won't be maintaining the fork. My new project is here: https://github.com/christopherthielen/ui-router-extras |
Hi, |
@christopherthielen Hi Chris, thanks for refactoring to work more like a plugin. I'm glad people can use this right away. Love it!! |
@timkindberg Thanks for the sentiment! I'm a little bummed this feature wasn't destined for main-line UI-Router, but glad that I was able to hack it into an addon. When it comes time to implement something like this in main-line, I'm happy to help work on it. |
@christopherthielen I'm eager to digest all the new features your plugin provides but at first sight it reminds me to some of the things from Ian Horrocks' statechart like history mechanism or concurrent states. Am I right about this? Anyway, it's great work. |
Plunkr: http://plnkr.co/edit/IgePmCtVnojo19i3y6Ab?p=preview
Updated Plunkr: http://plnkr.co/edit/YhQyPV?p=preview (2014-02-24, read comments posted on 2014-02-24)
Ui-Router Fork: https://github.com/christopherthielen/ui-router
I have a "Tabs" use case for Parallel States in each tab. Basically, my company has an non-angular application which users sit in all day long. There are multiple components, each of which lives in a separate tab, and each of which has its own complex nested state. Users switch back and forth between tabs to perform their job.
For a semi-theoretical example, let's say there are three tabs. Tab 1 is always an inbox, calendar, and task list. Tab 2 allows the user to create, view and update records and follow workflows. Tab 3 is swapped out with a variety of tools that do business work, such as billing or requisitions. Tabs operate independently, but the user needs to refer back to related data in tabs 1 and 2.
I want routing/url management, however if the user bookmarks a URL, I only care about bookmarking that particular tab. I do not need the state of all 3 tabs stored in the URL; I only need the state of the current tab reflected.
I have developed a proof-of-concept that shows how this could be implemented in ui-router. In my proof-of-concept, a ui-view may be tagged with "parallel-state" and multiple ui-view(s) may be added to the parent state's template. When a state transition occurs between the parallel branches, the "exited" state's locals and DOM is retained.
The plunkr demonstrates the parallel routes, via a tabbed UI. Click around in the UI (select "Show inactive tabs" to get a better idea of what is happening)
I'd like to hear comments and opinions about the approach. I'm new to ui-router, so I'm sure I might have leaked or misused some abstractions that I didn't understand. I have not yet tested or accounted for all ui-router functionality, like state parameters and resolves.
However, does this look like a reasonable approach to parallel routes?
I'd reference the other parallel state issues, if I knew how.
#475
#562
#63
#863
The text was updated successfully, but these errors were encountered: