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: Parallel states - Proof of Concept code #894

Closed
christopherthielen opened this issue Feb 17, 2014 · 75 comments
Closed

RFC: Parallel states - Proof of Concept code #894

christopherthielen opened this issue Feb 17, 2014 · 75 comments
Assignees

Comments

@christopherthielen
Copy link
Contributor

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

@timkindberg
Copy link
Contributor

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:

  • parallel views and states, that when active show their url in the address bar
  • the state will retain its data unless you leave its parent state, then if you return the data and view starts from scratch

The boilerplate gets us:

  • the showing/hiding of the tabs (see I think this should be more baked in)
  • The returning to child states within a tab (also wondering if this can be more baked in)

I wonder if instead of using a parallel-state directive we just have an extra parameter on the config and tap into the named view feature that already exists, so the children say what view they plug into instead of the view saying what child state will plug into it.

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.

@christopherthielen
Copy link
Contributor Author

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 <ui-view parallel-state=".S1">.

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, parallel-state=".S1" isn't actually a directive, it's just a data attribute that ui-view uses. I had at first used <ui-view=".S1"> but I thought that didn't provide an explicit "parallel" marker for readers of the markup, since parallel views are significantly different from <ui-view> and <ui-view="viewname">. When declaring a state with multiple named view references (note: not parallel states), those views are part of the state, as opposed to a "raw" <ui-view> which creates a placeholder for the next nested state to attach. <ui-view parallel-state=".S1"> is a hybrid of the two in that it uses the "raw" ui-view but is still parameterized, and doesn't confuse me as to what should happen. The "raw" nature of the ui-view tag (<ui-view parallel-state=".S1">) hints to me that it is not a placeholder for a view within the current state, but rather a placeholder for a child state. All that said, however, <ui-view=".S1"> feels reasonable to me as well.

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 <ui-view> is referencing the child state, implicitly (I realize this isn't really a good argument since multiple different child states could be loaded into that <ui-view>).

If we ran with your suggestion, parallel: true and parallelView: 'S1' are redundant. If we used only parallelView: "substate1" and then <ui-view=".substate1"> (note the dot notation to denote sub-state) would that smell better to you?

<ui-view=".substate1">

$stateProvider.state('root.tabs.S1', {
        controller: timerCtrl,
        template: 'whatever',
        scope: true,
        parallelView: 'substate1',
        url: '/s1'
    });

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:
(1) My first approach was to try with a directive that canceled $stateChangeSuccess event propagation.
(2) My second approach baked into ui-router a "parallel state container" concept, of which all direct substates were made parallel. This boiled down to creating a state (root.tabs) that was marked as something like parallelcontainer: true. I had problems accessing those substates' views when rebuilding the view tree (during directive compile and updateView() steps), but that was early in my exploration into ui-router and in hindsight, it may be an achievable approach.
(3) My third approach was a variation on my second, where I defined multiple named views on the parent state, as peers to standard named views. Those views then referenced the state they wanted loaded in that view. views: { S1: { state: 'root.tabs.S1' }, S2: { state: 'root.tabs.S2' } } Again, I had troubles loading the appropriate controller/template because I was trying to load locals, etc for children states and it didn't make sense.

Finally, I decided to try to embrace the currently built-in "raw" ui-view mechanism. I noticed that multiple <ui-view> <ui-view> would get loaded twice with the current state, so I tried short circuiting the load process when the parallel-state attribute didn't match the $current state. That way the correct state's ui-view is loaded when $current matches that state, but is not touched otherwise.

@christopherthielen
Copy link
Contributor Author

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.

I wonder if instead of using a parallel-state directive we just have an extra parameter on the config and tap into the named view feature that already exists, so the children say what view they plug into instead of the view saying what child state will plug into 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.

@christopherthielen
Copy link
Contributor Author

I converted the tab handler to directives and changed the ui-view syntax to <div ui-view=".substate">
Updated plunkr: http://plnkr.co/edit/OlBZkZ?p=preview

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>

@timkindberg
Copy link
Contributor

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:

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.

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 ui-view=".S2" though. That's not what I'd originally proposed, but I don't like what i originally proposed either anymore. I kind of like using the 'pipe' as seen in #863, but we don't need anymore special syntaxes. I wonder if just using a new directive called ui-parallel-view would be a good option?

<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.

@christopherthielen
Copy link
Contributor Author

Yes I think this is how it currently works. A parent state template can define multiple named views as such:
...
But its the child state that decides how to fill those shells:

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() {} }
   }
}

@timkindberg
Copy link
Contributor

Is this more along the lines of what you're thinking?

Yes it is. I'm still undecided if the substate should have to use the views property or if the ui-parallel-view directive can just specify the child state. Its more annoying to have to specify the views property but also more consistent API-wise with multiple named views.

I'll have to think more about this. Meanwhile hopefully some more people chime in before we go to far in any one direction.

@christopherthielen
Copy link
Contributor Author

Updated plunkr: http://plnkr.co/edit/YhQyPV?p=preview

I've made some more progress on this:

  • Migrated from ui-view-based logic to state transition-based logic.
  • Decoupled parallel code from main ui-router code (I extracted the bulk of the code into a $parallelState provider)
  • Added handling for state parameters and resolve
  • Added support for a parallel tree nested inside another parallel tree
  • Changed markup syntax to standard named view syntax

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:

So let me start with a disclaimer. UI-Router is in big flux right now with the bower issue, ... 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 ...

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 ... Anyway, if that's all good with you we can continue to explore this.

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 state

Normally, 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 inactivated

After 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;
          }

@timkindberg
Copy link
Contributor

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 retainDeep: true, which during reactivation will automatically navigate to the deepest child that was active before inactivation, retaining all locals as well. Just like you are already doing but basically using a state config boolean instead of a directive.

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:

  • Could a parallel state fill more than one named ui-view? Seems like it could/should. Would that currently work?
  • What is scope: true?

@christopherthielen
Copy link
Contributor Author

Tim,
I love the idea of baking in deep state re-activation as state configuration. This could be useful for non-parallel states too.

I made some changes, updated the plunkr, and committed src changes to my fork.

Baked in deep tab state re-activation

I 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, retainDeep, and I don't like any alternatives I've come up with either. For now my code is using deepStateRedirect but I'm sure there's a better name out there.

state.js integration rewrite

The 5 integration points remain the same, however I rewrote the code to hopefully be easier to comprehend.

I am now returning a parallelStateTransitionType object var ptType = $parallelState.getParallelTransitionType(keep, fromPath, toPath); which has { to: (bool), from: (bool) } to indicate if the transition is to and/or from a parallel state.

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)
var pEnterTransition = pEnterTransitions[l] = ptType && ptType.to && $parallelState.getEnterTransition(state, toParams, ancestorParamsChanged);
Enter transition can be either "reactivate" or "updateStateParams". During updateStateParams, we treat it as an exit/enter, not a reactivation. If an ancestor updated their state params, all descendants will also exit/enter.
ancestorParamsChanged = (pEnterTransition == "updateStateParams");
Now we have set up a nice way of checking how to load locals:

         if (pEnterTransition == "reactivate") {
           locals = toLocals[l] = $parallelState.getInactivatedState(state, toParams).locals;
         } else {
           locals = toLocals[l] = inherit(locals);
           resolved = resolveState(state, toParams, state === to, resolved, locals);
         }

And a nice way to determine if we should onExit or onInactivate:

          // Treat parallel transition type "updateStateParams" as an exit/enter
           if (ptType && ptType.from && pEnterTransitions[l] !== "updateStateParams") {
             $parallelState.stateInactivated(exiting);
           } else {
             $parallelState.stateExiting(exiting);

And a nice way to determine if we should onEnter or onReactivate:

          if (pEnterTransitions[l] == "reactivate") {
             $parallelState.stateReactivated(entering);
           } else {
             $parallelState.stateEntering(entering, toParams);
             if (entering.self.onEnter)
               $injector.invoke(entering.self.onEnter, entering.self, entering.locals.globals);
            }
questions

Could a parallel state fill more than one named ui-view? Seems like it could/should. Would that currently work?

Yes, I see no reason it wouldn't work currently but I haven't tried it.

What is scope: true?

Nothing to see here, move along. (copy/paste stupidity)

state transition thoughts

I 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.

@timkindberg
Copy link
Contributor

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 parallel to true or false, maybe set it to either "shallow" or "deep". That would eliminate the deepStateRedirect property.

I'm really surprised others haven't chimed in yet either.

@christopherthielen
Copy link
Contributor Author

  • I like "deep state redirect" as a separate option from the "parallel" option. I can imagine a use case for implementing non-parallel tabs system, where each tab gets rebuilt when activated, but the deep redirect is still used. parallel: false, deepStateRedirect: true
  • I had another thought about deepStateRedirect: true alternatively being set to something like deepStateRedirect: 'localStorage:myapp.mylasttabstate' or deepStateRedirect: 'cookie:myapp.mylasttabstate' and persisting stuff to the browser. Or what if it optionally took a function deepStateRedirect: myFunction that determined where to redirect to...

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?

No internal changes required, which means you have complete control over the implementation, which avoids future RFCs requesting additional configurability for whatever theoretical solution we might implement, which means I have more time and energy to spend implementing things this module isn't currently capable of, rather than merely making things that are already possible 10-lines-of-user-code easier.

  • Now that I've analyzed this problem more, does "parallel" conceptually still make sense, or does something more like "retain" better convey what is happening? The crux of the feature is as follows
    If a transition from some state A -> state C "pivots" over an ancestor state B where B is parallel: true, then retain the DOM tree of B and track the states behind that B tree as inactivated states.

@timkindberg
Copy link
Contributor

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.

@MrOrz
Copy link

MrOrz commented Mar 3, 2014

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.
It's a really exciting proposal and I found that the parallel option on states (in contrast of on "views" in #562) pretty flexible. @christopherthielen you did a great job! 👍

First Try on Modal Dialogs

The 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. parallel: true is only required on the state that lists all the items, as shown in this plunker -- The list page and the detail pages in action looks pretty good.

Follow Up

However, 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 parallel: true in their state config. In a website I am currently building, this literally means I need to add parallel: true to each and every states, except the one or two states that shows its view in modal dialogs.

Similarly, my proposal #562 suffers from the complexity in state configuration: the retain: true must be duplicated for each and every view for the states displayed in modal dialogs (for the example above, the states are root.detail and root.buy). State decorators may help, but the entire setup is still counter-intuitive.

Intuition When Dealing With UI

On 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 ui-view than in state config because when we deal with presentational user interfaces, we would point at a visible view in the physical monitor and say "hey, I want this block to stay unchanged", rather than seeing it from a state-machine perspective, which is more conceptual and abstract.

I suddenly realized that what I really need back in #562 should be a boolean attribute like ui-retain alongside with ui-view directive. With the presence of the ui-retain attribute, the ui-view never updates itself unless entering a state specifically assigns a new template / new resolve data to it, or when its parent view updates. Since it's a boolean attribute, we don't have to mix state information into view.

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.

@nateabele
Copy link
Contributor

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.

@christopherthielen
Copy link
Contributor Author

@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 comments

For 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 isChangeInParallelSubtree where parallel transitions weren't detected if they pivoted across the "implicit root state". I commited the fix to my branch and updated my plunk's angular-ui-router-tabs.js)

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:

implicit root
             |
     ------root ---------------
   /     /    \         \       \
detail  buy   popular   latest   suggest
              PARALLEL  PARALLEL PARALLEL

I sructured your states as follows, and got some reusability:

            implicit root
           /             \
        modals            products
        /    \            PARALLEL
     detail  buy         /     |   \
                      popular  |    suggest
                             latest
parallel declaration on state vs template/view

I 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 ui-retain ui-view at the whim of some parent template.

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.

@timkindberg
Copy link
Contributor

@MrOrz I know that the #562 kind of fizzled out, sorry about that. How do you feel about your implementation compared to this one? Thanks for taking a look!

@MrOrz
Copy link

MrOrz commented Mar 3, 2014

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 ui-retain idea.) Also the term "parallel" seems as if multiple states can be simultaneously activated in a single state machine, which confuses me a little bit.

@timkindberg
Copy link
Contributor

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?

@christopherthielen
Copy link
Contributor Author

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"

@timkindberg
Copy link
Contributor

Ah ok so that is the default. Its up to the dev to show/hide the appropriate views if they so choose.

@ashaffer
Copy link

ashaffer commented Mar 5, 2014

@christopherthielen @MrOrz

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.

@timkindberg
Copy link
Contributor

@ashaffer so you like it then?

@ashaffer
Copy link

ashaffer commented Mar 5, 2014

@timkindberg yes, very much.

@christopherthielen
Copy link
Contributor Author

@ashaffer cool 👍

@MrOrz
Copy link

MrOrz commented Mar 6, 2014

@ashaffer This is much better than my implementation of pinterest-like modal in #562 !
At first I would think it's straight-forward to hack ui-view and leave the states alone; now I start to appreciate the elegance to manage all the views by their states :)

@braco
Copy link

braco commented Mar 10, 2014

@ashaffer: Just FYI, I believe that implementation will incorrectly pop up a modal with:

  1. open modal
  2. reload for full page
  3. click anything
  4. click back in browser: modal pops up, full page expected

@ashaffer
Copy link

@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.

@gabrielmaldi
Copy link

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!

@christopherthielen
Copy link
Contributor Author

Last night I woke up from a dream, thought "maybe deepStateRedirect is just a special form of abstract: true", then fell back asleep.

abstract: "deepStateRedirect"
or
abstract: { deepStateRedirect: true }

@nateabele @timkindberg ?

@timkindberg
Copy link
Contributor

Hmm I understand the logic but seems a bit weird to mix those concepts.

@christopherthielen
Copy link
Contributor Author

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)

@nateabele
Copy link
Contributor

brb, reading academic papers about finite automata.

@DylanLukes
Copy link

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 and Transitions
  • Cursors (ui-views)
  • State Traversal References (ui-strefs)
  • Wildcard/Multi-source Transitions
  • Generalizing Transitions' from and to
  • Controllers and Data

States (ui-state) and Transitions (ui-sref... sort of)

States are modeled as a directed graph (or category if you prefer). Each state-node has a unique identifier, and a url format. The key here is that the actual state is not contained in the state-node itself. It can't be for reasons that will shortly become clear.

State transitions are declared via from-to relationships, rather than parent-child relations. It's worth noting that this fully generalizes the parent-child pattern. Conceptually, the graph contains directed edges to a failure node for every transition that is not explicitly defined. Realistically, this is a failover case.

We can incorporate virtual states easily in this model, which I'll get to once I discuss traversal paths.

Usage might look like this. Note that I'm also pulling the tempting behavior out, and allowing for functions as from, to arguments. A string argument should be implicitly parsed as a predicate, so the from field for tiddlywink is identical to just writing from: "foo". We don't actually have to explicitly build the graph, so this is fine.

$stateProvider
    .state("foo", {
        url: "/foo"

    })
    .state("bar", {
       url: _.partial(_.template, "/bar/{{x}}/{{y}}")
    })
    ...
    .transition("tiddlywink" {
        from: function (stateId) { return stateId == "foo"; } 
        to:   "bar",
        eager: false,
        on: function(cursor, from, to) { ... } 
    },

The eager attribute designates whether a transition should be forced to actually result in redisplay. The relevance of this will be seen shortly.

Cursors (ui-view), traversal paths (ui-stref)

Conceptually, a ui-view can be seen as a "cursor" containing a stack of transitions. Note that it's a stack of transitions, not states! There might be multiple valid transitions from one edge to another.

As it stands, ui-srefs refer to a state, and when one is activated, the unique transition to that state is applied. Let's introduce a new concept, generalizing ui-sref, called a ui-stref. We will soon elaborate on how to implement ui-sref (and virtual states!) by allowing wildcard transitions.

A ui-stref is an attribute containing a list of state transition IDs. Activating a ui-stref attempt to apply each state transition in turn. Until the final state is reached, transitions are applied as a "dry run". If any errors or invalid transitions are found, the entire transition is rolled back to the last eager: true transition. eager means that redisplay must be forced before applying the next transition in the ui-stref. It's similar to a Prolog cut (!). Clearly, this is somewhat unsafe behavior, but it has its uses.

Each ui-view is in essence a cursor traversing this tree, that keeps track of the transitions it's followed through the state graph. This allows for unrestricted nesting and recursion of states. While these can be abused, they are also very powerful and useful.

Multi-source and Wildcard transitions

Clearly, a transition isn't immediately that useful if it's only valid from a single state. For most menus and such, we wish to jump to the root state for the menu items, then traverse to the desired state. We can model this using multi-source and wildcard transitions. A wildcard transition is defined as follows:

    $stateProvider.transition("knickknack", {
        from: "*", 
        to: "someState",
        via: ["transition1", "transition2", ..., "transitionN"]
    })

Going back to looking at ui-views as cursors, a wildcard essentially unravels the cursor's traversal history and resets it. from: "*" is shorthand for from: function() { return true; }. The via parameter designates what to reset the traversal to. "transition1" must be valid from the root state, and the final transition must be valid for the to destination.

For multi-source transitions, just provide an array of source states to to.

Aside: Generalizing from and to

We can actually reasonably generalize from and to entirely to incorporate all of the previously described behaviors succinctly.

from is a predicate. The current state is already known, so from must simply decide whether the transition can be applied in the first place. So for shorthand we have:

  • from: "foo"from: function(s) { return s == "foo" }
  • from: ["foo", "bar", ...]from: _.partial(_.contains, ["foo", "bar", ...])
  • from: "*"from: _.constant(true)

to is a decision. Once the from predicate has passed, we must now select the destination. This is easy when we have a simple to (i.e. just a destination state). For any more complicated cases with multiple potential destinations, we cannot make a decision without more information. So, we might allow to's to accept a function of the cursor and the current states data.

Abstract States

Abstract states fit nicely into the above description. They're just wildcard states with a special to function, which "forwards" using data stored in the cursor. Think of the cursor as putting itself in a box, stamping a label on it ("this is where I want to go") and letting itself go into the system. A virtual state must send a cursor onward based on where it wants to go, or a rollback occurs.

When an stref is activated, the entire path should be stored on the cursor as pending and iterated through part by part. The remaining portion of the path should be passed into the to function in some manner to enable this behavior.

Aside: an eager transition into a virtual state could be potentially nasty. Thus, eager transitions can't apply until the transition actually completes.

Controllers and Data (and finally enabling parallel states!)

Logically, it makes sense for each state to potentially have a controller, and for each cursor to be mandated to have one. The remaining difficult issue in this model is handling how state parameters and miscellaneous data are resolved. Some data should be carried along with the cursor, while other data should remain in a particular state.

Moreover, some data is only relevant to a particular cursor/state pairing. I think the proper way to handle this is to inject services into these controllers and centralize storage of state/cursor data. As with most mathematically informed models, data-state is where things start getting a bit tough.

Anyhow, I hope this has been an interesting proposition. I'm going to try to implement a proof of concept to demonstrate how this works, which I'll post here when finished.

Cheers, 🍺

@CMCDragonkai
Copy link

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.

@nateabele
Copy link
Contributor

@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.

Furthermore I would suggest to make state transitions as promises, so they can be processed asynchronously.

@CMCDragonkai I don't follow. State transitions already are promises.

@CMCDragonkai
Copy link

Yep I know. Just for with regards to Dylan's ideas.

@DylanLukes
Copy link

A few more thoughts (skip to the bottom for the more interesting ones):

  1. Yes, my design definitely loses the rigor. It essentially allows you to defer the actual construction of a state-tree/graph and handle it dynamically. I should qualify that: I don't think that's actually useful for most real world use-cases. What I'm concerned with is providing a set of fundamental pieces that can be linked up and sugared into routers optimized for different topologies by imposing certain constraints. For example:

    Hierarchical (Tree) Routing
    The constraints here are that there cannot be any explicit(!) backwards transitions, states must be 1:N with respect to their outgoing transitions, and have a unique incoming transition. Of course, in reality we do traverse the tree backwards. I think it's more intuitive to model backwards transitioning by roll-backs. Moreover, we often do things like jump from one branch to another while implicitly traversing back to the common ancestor and then back down...
    Linear/Cyclic Routing
    This one's actually more common than you'd think: slide-shows. You can look at it as a special case of tree-shaped routing, in which case the "roll-back" behavior is better elucidated. Working with a slide-show, there are stateful effects within each slide, which are entirely rolled back when the presenter goes back to a previous slide. We don't actually need to allow for state *in the slides* to have "sticky" behavior: slides stay the way they were when last visited. We just need to add one link from the last slide to the first, and keep the state in the traversal itself. Of course, this can lead to some technical inefficiency as a jump back one slide actually incurs N forward traversals (where N is the length of the slideshow), but that should be optimized out anyhow (sparse traversals? partially deferred evaluation?).
    Composite Routing
    This is what motivated me to start thinking about this problem. I have an application I'm building (with ui-router of course) which contains tabs, each of which contains instances of sub-applications with distinct routing schemes (one is a presentation tool, another is a sort of contact book, etc). By imposing any of the above sets of constraints too harshly, it becomes difficult to compose them nicely. So, I think the solution is to abstract away the constraints and right interoperable components that can be used to define both. A couple visualizations: a tree with cyclic "fruit", a cycle with trees growing from its elements, and so forth. And what about direct products of cycles?

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 Scope

This 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.

@DylanLukes
Copy link

Some further stuff that's concerning me:

  • Allowing cycles makes moving "up" or "back" a very non-trivial problem. You have to know in advance how to do so... I'm worried going back through the current path step by step might not be good enough.
  • I think I might actually need to make the state-graph defined statically. What might work as a substitute for dynamic addition is embedding graphs inside other ones. But again, this doesn't play nicely with backtracking. And there are definite needs for dynamic behavior: parameterized states, tabs, etc...

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 :).

@DylanLukes
Copy link

Just finalized the core data structure for my proof-of-concept. It's basically a generic graph with the following features:

  • Edges may have arbitrary data associated with them. They contain source/target references, but these are just a convenience and not strictly necessary. I'll probably remove these fields and make them internal lookups hidden behind properties to try to reduce the risk of shooting yourself in the foot by modifying edges.
  • Nodes may be literally any data type. They don't have any graph state in them at all. However, uniqueness is enforced (modulo ES6 egal).
  • DAG structure is enforced by a naive implementation of the Pearce-Kelley online toposort. In other words, if you try to create a cycle you get an exception.

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:

@ashaffer
Copy link

@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.

@christopherthielen
Copy link
Contributor Author

@ashaffer thanks! I moved parallelSupport inside $get and injected the $injector there.

@davereed
Copy link

davereed commented Jun 9, 2014

@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?

@christopherthielen
Copy link
Contributor Author

@davereed sorry, that's outside the scope of this experiment. The back button drives the browser URL (which then drives state). The state machine itself doesn't really have a history capability yet. See issue #92

@ashaffer
Copy link

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

@christopherthielen
Copy link
Contributor Author

@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?

@ashaffer
Copy link

@christopherthielen Ya, I just stick parallel: true on my modal states.

@christopherthielen
Copy link
Contributor Author

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

http://christopherthielen.github.io/ui-router-extras/

@snekbaev
Copy link

Hi,
@christopherthielen thank you very much! I was waiting if it will be integrated, but no matter what planning on using it in near several weeks. Thanks again!

@timkindberg
Copy link
Contributor

@christopherthielen Hi Chris, thanks for refactoring to work more like a plugin. I'm glad people can use this right away. Love it!!

@christopherthielen
Copy link
Contributor Author

@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.

@apreg
Copy link

apreg commented Jul 14, 2014

@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.

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