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

Manage routes/states in views independently #160

Closed
paullryan opened this issue Jun 1, 2013 · 27 comments
Closed

Manage routes/states in views independently #160

paullryan opened this issue Jun 1, 2013 · 27 comments

Comments

@paullryan
Copy link

There was comment in issue #84 (#84 (comment)) in which @FAQinghere offhandedly mentioned the potential for managing views independently. Also recently the topic of managing multiple independent widgets or portlets has come up on Stack Overflow. I think it's worth discussing whether there is merit to have the ability in the ui-router (and thus, hopefully, in a future version of angular core) to manage views as independent entities that are not necessarily tied to the url but instead are tied to a state map that is instantiated for a given top level view.

@laurelnaiad
Copy link

+1 for independent widgets/portlets -- I guess the tie in with the router is that the router/states would need to be shared (nicely) by all of the widgets/portlets on a page? I'm having trouble wrapping my head around the implications...

@paullryan
Copy link
Author

@stu-salsbury yep I'm thinking it would literally have to be a graph to track the independent branches of state changes. Using the ancestor path of a given state as the key as to whether to apply it to a general ui-view or a given path's ui-view. The model for state path tracking to me starts to feel a lot like the XPath data model (XDM, http://www.w3.org/TR/xpath-datamodel/). The ancestor axis model defined within XDM maps pretty well and then corollary to that the view maps to the concept of an attribute of a given point in the path.

Note: I'm not advocating that we in any way move towards xpath syntax just that I believe the XDM working group may have solved the majority of the problem of discovering the ancestor properties for us

@laurelnaiad
Copy link

Funny you mention XPath. I'm working on a CMS based on XQuery/Marklogic
Server.

Could you explain how the XPath ancestor axis relates to support for
independent branches/states and widgets/portlets? I think you lost me there.

@paullryan
Copy link
Author

Sorry about that my day job consists of lots of large loosely structured xml (100+ MB of text in engineering manuals) so that model is pretty much what I deal with all day. Let me see if I can break it down.

So if we look at the current model of states and represent it at as a tree for a state set like the following:

$stateProvider
    .state('xyz', {
        url: '/'
    })
    .state('abc', {
        url: '/abc',
        abstract: true,
        templateUrl: 'partials/abc/index.html'
    })
    .state('abc.overview', {
        url: '/overview',
        templateUrl: 'partials/abc/overview.html'
    })
    .state('abc.multi', {
        url: '/multi',
        templateUrl: 'partials/abc/multi.html'
    })
    .state('abc.multi.route1', {
        url: '/route1',
        views: {
            "side1": {
                templateUrl: 'partials/abc/route1/side1.html'
            },
            "side2": {
                templateUrl: 'partials/abc/route1/side2.html'
            }
        }
    })
    .state('abc.multi.route2', {
        views: {
            url: '/route1',
            "side1": {
                templateUrl: 'partials/abc/route2/side1.html'
            },
            "side2": {
                templateUrl: 'partials/abc/route2/side2.html'
            }
        }
    });

You end up with a tree something like (I'm sure I'm not representing this the way it's done internally but I'm trying to make the simplest tree I can to show the XDM model application):

[
    {
        name: 'xyz',
        url: '/',
        children: []
    },
    {
        name: 'abc',
        url: '/abc',
        templateUrl: 'partials/abc/index.html',
        children: [
            {
                name: 'overview',
                url: '/overview',
                templateUrl: 'partials/abc/overview.html',
                children: []
            },
            {
                name: 'multi',
                url: '/multi',
                templateUrl: 'partials/abc/multi.html',
                children: [
                    {
                        name: 'route1',
                        url: '/route1',
                        views: [
                            {
                                name: "side1",
                                templateUrl: 'partials/abc/route1/side1.html'
                            },
                            {
                                name: "side2",
                                templateUrl: 'partials/abc/route1/side2.html'
                            }
                        ],
                        children: []
                    },
                    {
                        name: 'route2',
                        url: '/route2',
                        views: [
                            {
                                name: "side1",
                                templateUrl: 'partials/abc/route2/side1.html'
                            },
                            {
                                name: "side2",
                                templateUrl: 'partials/abc/route2/side2.html'
                            }
                        ],
                        children: []
                    }
                ]
            }
        ]
    }
]

Given this view if we want to associate children to the views we would have to have a secondary view lookup and this makes it quite difficult to have the views be driven from different angular modules that aren't aware of the root parent hierarchy. What I'm looking at is that if we move to a model more like the following:

$stateProvider
    .state('xyz', {
        url: '/'
    })
    .state('abc', {
        url: '/abc',
        abstract: true,
        templateUrl: 'partials/abc/index.html'
    })
    .state('abc.overview', {
        url: '/overview',
        templateUrl: 'partials/abc/overview.html'
    })
    .state('abc.multi', {
        url: '/multi',
        templateUrl: 'partials/abc/multi.html',
        views: {
            "side1": {
                childState: "foo"
            },
            "side2": {
                childState: "bar"
            }
        }
    })
    .state('foo', {
        root: false,
        templateUrl: "partials/foo/index.html"
    })
    .state('foo.internal1', {
        templateUrl: "partials/foo/internal1/index.html"
    })
    .state('foo.internal1.sub1', {
        templateUrl: "partials/foo/internal1/sub1.html"
    })
    .state('foo.internal1.multi', {
        templateUrl: "partials/foo/internal1/multi.html",
        views: {
            "foochild1": {
                childState: "foochild1"
            },
            "foochild2": {
                childState: "foochild2"
            }
        }
    })
    .state('foochild1', {
        root: false,
        templateUrl: "partials/foo/foochild/child1.html"
    })
    .state('foochild2', {
        root: false,
        templateUrl: "partials/foo/foochild/child2.html"
    })
    .state('bar', {
        root: false,
        templateUrl: "partials/bar/index.html"
    })
    .state('bar.internal1', {
        templateUrl: "partials/bar/internal1/index.html"
    })
    .state('foo.internal1.sub1', {
        templateUrl: "partials/bar/internal1/sub1.html"
    })

With a generated model that looks something like:

[
    {
        name: 'xyz',
        url: '/',
        children: []
    },
    {
        name: 'abc',
        url: '/abc',
        templateUrl: 'partials/abc/index.html',
        children: [
            {
                name: 'overview',
                url: '/overview',
                templateUrl: 'partials/abc/overview.html',
                children: []
            },
            {
                name: 'multi',
                url: '/multi',
                templateUrl: 'partials/abc/multi.html',
                children: [
                    {
                        name: 'foo',
                        view: "side1",
                        templateUrl: "partials/foo/index.html",
                        children: [
                            {
                                name: 'internal1',
                                templateUrl: "partials/foo/internal1/index.html",
                                children: [
                                    {
                                        name: 'sub1',
                                        templateUrl: "partials/foo/internal1/sub1.html",
                                        children: []
                                    },
                                    {
                                        name: 'multi',
                                        templateUrl: "partials/foo/internal1/multi.html",
                                        children: [
                                            {
                                                name: 'foochild1',
                                                view: "foochild1",
                                                templateUrl: "partials/foo/foochild/child1.html",
                                                children: []
                                            },
                                            {
                                                name: 'foochild2',
                                                view: "foochild2",
                                                templateUrl: "partials/foo/foochild/child2.html",
                                                children: []
                                            }
                                        ]
                                    }
                                ]
                            }
                        ]
                    },
                    {
                        name: 'bar',
                        view: "side2",
                        templateUrl: "partials/bar/index.html",
                        children: [
                            {
                                name: 'internal1',
                                templateUrl: "partials/bar/internal1/index.html",
                                children: [
                                    {
                                        name: 'sub1',
                                        templateUrl: "partials/bar/internal1/sub1.html",
                                        children: []
                                    }
                                ]
                            }
                        ]
                    }
                ]
            }
        ]
    }
]

We end up with an xml representation of this like the following (only used to illustrate the XDM tree walking with xpath):

<states>
    <state name="xyz">
        <url>/</url>
    </state>
    <state name="abc">
        <url>/abc</url>
        <templateUrl>partials/abc/index.html</templateUrl>
        <state name="overview">
            <url>/overview</url>
            <templateUrl>partials/abc/overview.html</templateUrl>
        </state>
        <state name="multi">
            <url>/mutli</url>
            <templateUrl>partials/abc/multi.html</templateUrl>
            <state name="foo" view="side1">
                <templateUrl>partials/foo/index.html</templateUrl>
                <state name="internal1">
                    <templateUrl>partials/foo/internal1/index.html</templateUrl>
                    <state name="sub1">
                        <templateUrl>partials/foo/internal1/sub1.html</templateUrl>
                    </state>
                    <state name="multi">
                        <templateUrl>partials/foo/internal1/multi.html</templateUrl>
                        <state name="foochild1" view="foochild1">
                            <templateUrl>partials/foo/foochild/child1.html</templateUrl>
                        </state>
                        <state name="foochild2" view="foochild2">
                            <templateUrl>partials/foo/foochild/child2.html</templateUrl>
                        </state>
                    </state>
                </state>
            </state>
            <state name="bar" view="side1">
                <templateUrl>partials/bar/index.html</templateUrl>
                <state name="internal1">
                    <templateUrl>partials/bar/internal1/index.html</templateUrl>
                    <state name="sub1">
                        <templateUrl>partials/bar/internal1/sub1.html</templateUrl>
                    </state>
                </state>
            </state>
        </state>
    </state>
</states>

Given this kind of model if we were had a page that a page then that is currently at the following state:
github_router_independent

Then we get a request to change transition to state foochild2 the xpath for figuring out where to transition this state could look something like //state[@name='foochild2']/ancestor::state[@view][0].

Again I'm not advocating for using an xml model but I'm looking at how XDM solved this kind of ancestor walking in a tree and seeing some helpful patterns we could use.

@paullryan
Copy link
Author

Oh as a side there has been some talk (http://www.balisage.net/Proceedings/vol8/html/Rennau01/BalisageVol8-Rennau01.html) about a unified description language to codify the usefulness of XDM for other tree structures (namely JSON). I know Hans is looking pretty heavily into what this means and would love to talk to anyone that wants to help (he and I had a long conversation after his talk last year at balisage).

@laurelnaiad
Copy link

Side note noted! I'll look into that. It might come in handy when mixing server-side XML with client-side Javascript. JsonPath looks related to the side note. But if we leave all things XML out of it for the time being...

Since you're mentioning trees, I've actually put together a tree-based offshoot of ui-router called detour, which happens to be tree-based because it makes it easier to support editability and JSON representations of state/route definitions. However, I'm not clear on how the presence or absence of a tree-based router is related to the problem at hand in this issue.

I think you're stating that the problem you're trying to solve is that:

it [is] quite difficult to have the views be driven from different angular modules that aren't aware of the root parent hierarchy.

Could you elaborate on that problem statement or any other problem statement that might help to express your intent?

@ksperling
Copy link
Contributor

It seems the original issue that this was discussed in was closed (because the discussion wandered off to a bunch of other topics: #123), so here is my comment on that again:

@graphicsxp yes, that's right, all the dialog's states would be inside the caller's state, and while the user is navigating through the dialog the caller state would remain active.

It seems cases like this could benefit from having a way to define a "blueprint" for a re-usable sub-tree of states, which can then be "instantiated" in multiple places in the state tree. Maybe we could call it a "component" instead of a blueprint, and allow them be bound to a view in a state, along the lines of

$stateProvider
  .component('contactsearch', {
    template: ...,
    controller: ...,
  })
  .state('contactsearch.step1', {
    url: '/step1/:param',
    ...
  })
  .state('contactsearch.step2', {
    ...
  })


$stateProvider.state('abc', {
  url: '/abc/:abcParams1
  views: {
    'details': {
      component: 'contactsearch'
    }
  }
});

The root state of the component would effectively need to be merged into the calling state, and child states of the component become children of the calling state (if we'd want to allow multiple components to be attached to the same state we're going to have to look at what's effectively "orthogonal regions" in the state machine, but then this is a feature that's been hinted at a few times)

All URLs within a component would need to be relative, so that they can be combined with the URL for any parent state. Because the root state of the component gets merged into the caller, it can't have it's own URL (or state parameters), but it's children could. We'd probably have to do some magic so a component gets it's own separate namespace for parameters and it's own $stateParams.

In fact if we do allow multiple "orthogonal" components, the calling state might have to explicitly provide a URL parameter that the entire URL of anything that happens within the component's own state machine gets serialized into, e.g. the calling state's URL pattern might look like '/abc/{abcParameter}/?details' if using a query parameter, or '/abc/{abcParameter}/{details:.*}' with a path parameter -- with the convention being that the parameter name matches the name of the view that the component is instantiated into. In most cases there would have to be some encoding/escaping (e.g. percent-encoding) applied to the component's URL, depending on how it's inserted into the parent URL. (I think I'll stop here... there's a lot more detail that would need to happen)...

@paullryan
Copy link
Author

@ksperling I like your syntax for this a lot more that the thrown together one I had above. Are you ok with this issue staying open as the central one for this issue?

@ksperling
Copy link
Contributor

Yeah, let's leave this one open and not have the discussion wander onto dynamic loading ;-)

@laurelnaiad
Copy link

I may be oversimplifying, but would it be fair to say that each component would get a piece of the URL-space for itself, and that one of the keys would be to figure out how to "delimit" the pieces of the URL? Sort of like overcoming the hierarchical-ness of URLs?

@timkindberg
Copy link
Contributor

I like your syntax as well @ksperling.

@stu-salsbury yes I was thinking that would be needed as well. Obviously it wouldn't be needed for url params after a '?' because that's what '&' is for, but for "in-url" params maybe we prepend each component piece with double underscores? They certainly make each orthogonal route very readable. Not sure if the underscores would interfere with something else though, like if double underscores were used in a state url.

I imagine a url that would end up looking like:
abc/abc123/__step1/xyz

So if we had more than one component it would look like:
abc/abc123/__step1/xyz/__route1/subroute/xyz

or maybe even include the names in the url as well?
abc/abc123/__details=step1/xyz/__other=route1/subroute/xyz

Also I wonder if we'd want a special component parameter syntax for defining urls more easily and clearly, such as:
state url: abc/:abcParams/{component:details}/{component:other}

@laurelnaiad
Copy link

Thinking out loud:

What if component parameters were not in any way defined to map to URLs within the component?

So if we're doing something differently for a component, it's because they require more isolation (which seems to make sense given that they may come from elsewhere and be reused with other components which may use the same parameter names.

In some cases, the same value might actually be the one that both components want to "use" and it would be a shame to repeat its value in the url and it would be a shame to ignore the fact that they're really trying to share that value.

So what if the state that instantiates the components is in charge or the URL altogether, and that includes mapping to the parameters each component needs? In theory, the components could then even share a single URL parameter.

So for config:

.component('person', {
  params: {
    'firstName': '{[A-Z]*}',
    'lastName': '{[A-Z]*}',
    'opinionOnHockey': '.*'
  }
  //...
})

.component('business', {
  params: {
    'businessName': '{[A-Z]*}'
    'hockeyThoughts': '.*'
  }
  //...
})

.state('stateMain', {
  url: '/small-business/:name/customers/:last/:first/hockeyLongStory/:hockeyDrama',
  components: {
    'cust': {
      'person': {
        params: {
          'firstName': 'first',
          'lastName': 'last',
          'opinionOnHockey': 'hockeyDrama',
        }
      }
    },
    'bus': {
      'business': {
        params: {
          'businessName', 'name',
          'hockeyThoughts', 'hockeyDrama'
        }
      }
    }
  }
})

The url /small-business/Crabshak/customers/Colbert/Stephen/hockeyLongStorey/Je%20parles%20francais

Would correspond to

  • component person having firstName: 'Stephen', lastname: 'Colbert' and opinionOnHockey: I speak french
  • component business having businessName: 'Crabshak', hockeyThoughts: 'Je parles francais'

Please don't get overly caught up in the syntax... I'm just trying to demonstrate the idea of letting the state that is instantiating the components be in charge of the parameter mapping... some thought is required to address concerns about the two components sharing their views on hockey, since hockey fans can be boisterous, to put it gently.

The reason I gravitate toward this approach is that it feels syncopatic with tried and true GUI development kits' approaches to "widgets".

However, I can't seem to shake a nagging feeling that going down this path might be reinventing the wheel. Is it possible that "componentizing" should be handled in controllers and directives? To the extent that URLs are involved, the router clearly needs to be friendly to components, right? So I think it makes sense to help with mapping URLs to the parameters a component wants/needs...

@ksperling
Copy link
Contributor

@stu-salsbury the problem with letting the "outer" state handle the mapping is that you have to cater for the product of all the component states, e.g. if there are two components with 5 states each that is 25 actual states.

The solution I was thinking of is to have the "outer" state define one parameter per component, and then the "URL" of the states inside the components gets serialized into that one parameter (the default encoding/decoding behaviour for parameters is encodeURIComponent (but it is planned to allow this to be overridden via custom parameter types), so there is no issue with delimiters getting mixed up between "levels")

The advantage of this approach is that everything should "just work", even though it might still require some manual work if you want the URLs to look neat (even though IMO at the point where the state of multiple, possibly nested components has to be serialized into the URL they will be big and messy no matter what).

@ksperling
Copy link
Contributor

@stu-salsbury regarding the overall componentization, I think the problem is that directives by themselves are just not powerful enough to be "the" component abstraction in Angular. Directives are just too tied to the template compilation/rendering process and their own place within the tree to e.g. control when and where the directive appears based on URL state.

I think a ui-router component (along with it's own controllers and possible even directives) can be a useful level of abstraction and reuse.

@laurelnaiad
Copy link

@ksperling, I see your point about 5x5=25... and I understand that by
putting the properties that need to be communicated to the components into
query parameters, you could isolate the components' needs from the state
that owns them...

However, is 5x5=25 really the normal use case? I was thinking the typical
use case would be more along the lines of a set of components each of which
does not have 5 parameters (5 is a lot of parameters for one page on its
own!), and where there is non-trivial overlap between the values that need
to be plugged into them. For example, if you're on a user preferences page
and you have 5 components where each handles a portion of the preferences,
they'd really only need one URL parameter (userid) to be allocated by the
owning state to the owned components....

The more I think about it, to me it seems a component shouldn't care about
states any more than a Java widget would worry about the name of the form
into which it's dropped.... it cares about properties... and the
router/state manager should help get the properties assigned to the
components like a form would do for its widgets...

On the other note... yes, I agree. The more we bat this around, the
clearer it is that it's useful for the state manager to get involved with
component states/parameters/properties.

@ksperling
Copy link
Contributor

@stu-salsbury I'm not talking about the number of parameters the component has, but the number of internal states in the component (each of which may or may not have parameters)

@laurelnaiad
Copy link

I guess we're talking about different kinds of components? I didn't envision components having states defined inside of them (though I could see them having other components inside of them). I guess I was thinking of components as being distinct from states, even though they would be managed by them -- again, like widgets on a form -- no "life" of their own without a container (a state). A state would be a souped-up component (like a form is a souped up widget) -- souped up to handle routing and state management, in this case.

@paullryan
Copy link
Author

@stu-salsbury and @ksperling I definitely agree with where Karsten is going with this as the component having it's own state as I was showing in my above comment. If you think abut the use case of portlets, each portlet is effectively a separate application being controlled in a small part of your overall application. This means that this portlet/component would have it's own defined state, controllers, directives, etc. The traditional way of handling this is with iframes so there is a complete isolation and it can be pulled from the server independently. However with the ui-router we aren't that far from controlling these sub-modules from within a top level state framework that integrates their common services and parent transitions.

@laurelnaiad
Copy link

I totally agree that @ksperling's approach is better for portlets (assembling a set of UI elements that may have little to do with one user task, and where the parent does not become significantly involved with their operation.

I think support for widgets (as opposed to portlets) would be useful, too -- i.e. things that, as a (potentially hierarchical) group, are part of one user task, in which case it seems that they shouldn't have their own state. It's probably best not to conflate the two.

@ksperling
Copy link
Contributor

@stu-salsbury hm yeah a separate "widget" abstraction may be useful. It would need some thought on how such a "widget" differs from (1) a directive or (2) a component that happens to have only one state -- i.e. is there enough conceptual room between those two things to warrant introducing "widget" as an abstraction of it's own.

@laurelnaiad
Copy link

@ksperling -- yes, I'm wondering the same thing. Passing state parameters as properties of a widget isn't really solving any problems -- any sort of container/widget hierarchy would need controllers at both levels, anyway, and scope inheritance is already there.

Inheritance for directives would be useful -- and totally unrelated to routers! :)

@FoxxMD
Copy link

FoxxMD commented Aug 14, 2014

+1 on this. It would be extremely useful for menus that also display content -- say I have a sidemenu with regular location links etc, but then I also have a content area in that menu. I want to hit /login or `/register' and have that content show up in the side ui-view and have everything else dictated by the main ui-view. They would be parallel in essence.

untitled

@mikehaas763
Copy link

What @FoxxMD mentioned is exactly the type of functionality I've been trying to squeeze out of ui-router lately. Doesn't seem like it's something that's going to work though. My new direction until something better comes out is to just create a directive that sits on the root (index) view for the login/register area.

@FoxxMD
Copy link

FoxxMD commented Aug 27, 2014

@mikehaas763

What I ended up doing was use a combination of event watchers and and the new optional params from #1032 to activate opening the menu and show the right content based on the end of the url.

I used the optional params with a regex expression to filter down to only the actions I wanted, however I had to create this as as an extra state of the "parent" actual state I wanted it to affect.

.state('eventSkeleton', {
    templateUrl: '/views/shared/skeleton.html',
    url: '/event/{eventId:[0-9]}',
    params:{
        eventId: {}
    },
    abstract: true,
    controller: 'EventController as eventCtrl',
    parent: 'index'
})
.state('eventSkeleton.event', {
    url:'/{opt:(?:login|register)}',
    params:{
        opt: {value:null},
        eventId: {}
    },
    templateUrl: '/views/event/eventHome.html'
})
.state('eventSkeleton.EventTeams',{
    url:'/teams',
    parent: 'eventSkeleton',
    templateUrl:'/views/teams/teamHome.html'
});

This allows me to use /login or /register at the end of state's URL, but have it be optional. EX website.com/event/2/login or website.com/event/2

Then in the link function of my menu directive I watch for $stateChangeSuccess and if I find the opt param trigger the correct action

$rootScope.openLogin = function() {
    $rootScope.toggleMenu();
    $scope.sidebar.loginVisible = true;
};
$rootScope.openRegister = function() {
    $rootScope.toggleMenu();
    $scope.sidebar.registerVisible = true;
};
//Watches for URL change and if it finds
// /login or /register at the end of the URL opens the sidemenu and respective action
$rootScope.$on('$stateChangeSuccess', function(event, toState, toParams, fromState, fromParams){
    switch(toParams.opt)
    {
        case('login'):
            $rootScope.openLogin();
            break;
        case('register'):
            $rootScope.openRegister();
            break;
    }
});

Of course this wouldn't be ideal if my menu was supposed to have full-fledged navigation/content features but since I am just using the "content" piece of it for logging in and registration it works well enough for me.

@ProLoser
Copy link
Member

I think this can be closed since you can specify views on any state level you want which is how I tackle this.

@mikehaas763
Copy link

@ProLoser That's not really the solution that's being looked for here... AFAIK. You can specify the views on any state level, but if you have parallel views, each state needs to specify the views for both view areas. Although, I agree I doubt a more permanent solution is going to come out of this issue. Might as well be closed.

@ProLoser
Copy link
Member

I'm a little confused as to the nature of this issue (since it's huge) however you do not HAVE to specify a view, you can just leave it empty and let it get populated later by a child view. I personally do this quite a bit. Note here that my parent state 'projects' does not populate the header, but once you open a project the 'projects.view' state DOES: https://github.com/ProLoser/AngularJS-ORM/blob/a2a7960569add0ee51e02e6769396510128a418b/modules/Project/Project.coffee#L24-L32

@christopherthielen christopherthielen removed this from the 1.5.0 milestone Nov 16, 2014
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

8 participants