angular-ui/ui-router

Abstract States, Non-Abstract Parent States and Default Child States

timkindberg opened this issue ยท 43 comments

I know we went back and forth on this several times already, but I think I may have found a need for this feature again.

It seems as though we are trying to get to a place where states can function completely on their own without any url routing (correct?). So how would we know which child state to load automatically without specifying a url of ''? Seems like we would need a default boolean property or something similar.

Let me also attempt to define abstract states, and non-abstract parent states. Please let me know if I'm on the right track.

Abstract State

  • must be a parent state
  • a state that cannot be activated directly (via transitionTo method) without activating one of its children.
  • must have a default child state (either with empty url or default property)
  • if no default child state is set, first child state found will be used as default
  • can be navigated to via url, but this really just activates default child state

Non-Abstract Parent State

  • a normal state which has child states
  • can be activated and navigated to directly (via transitionTo method or url), though any view directives will not be populated until navigation to on of it's child state OR one of its child states is set as default child (either with empty url or default property)
  • default child state is optional

Yes, that's essentially whats currently implemented, minus the idea of a "default child".

In the sample, 'contacts' is abstract, so $state.transitionTo('contacts', ...) simply throws an exception. To go to 'contacts.list' via code you just $state.transitionTo('contacts.list', ...).

It is tempting to call 'contacts.list' the "default child" of 'contacts', because it uses URL '', however another way of looking at it (and this is how the current implementation treats it), is that there is no requirement for the URL of a child state to begin with the URL of it's parent state, and all state URLs are completely independent, "absolute" URLs (that get added to $urlRouterProvider).

The ability to define state.url relative to the parent state's URL is simply a convenience provided by $stateProvider.state(), because it's very common thing to do (and there is an 'escape' mechanism of prefixing the URL with '^' to tell $sP.state() to treat it as absolute).

In the sample app, it would be completely possible to change the URL of contacts.list to '^/list' (i.e. the absolute path '/list'), or to completely remove the URL from the 'contacts' state and instead copy&paste the '/contacts/' prefix into all it's child states URLs. Would you still call contacts.list the default child of contacts in that case?

Well I do see your point, I guess I just thought if someone can theoretically set a default child via an empty URL then they may also want the same ability when not using urls. I see how it's not as big of a deal as I initially imagined though so I'd say this is more a feature request now than an emergency. Will put in v.2.

I'd like to have abstract child states - basically for having a different "layout" that would be used. For example, a layout that is the default one, .state('default'); .state('default.contacts') .state('default.contacts.list') and then a .state('compact') .state('compact.contacts') .state('compact.contacts.list')

Where "default" and "compact" are different layouts - the reason for me being that there might be two different views for a child layout - ie; one inside of the main layout, and then the same one in a popout window, looking a bit different, but the same view being used. But in my case, the two views are nearly the same, just with some elements "turned off".

Right now I've done thing by splitting a lot of stuff, but the base templates for each controller violate DRY, and one change in one template piece that's shared will have to be changed across multiple files. I'm struggling to find a more efficient way to do this. I'm thinking of nested views, but having trouble conceptualizing on how I would organize it, maybe that's the way to go.

Nevermind, after reading about it some more, I think nested views are what I need to go. Just have to figure out the best way to organize it all. Maybe a ui-view for the body, and just sub-views inside of that view to put it all together.

jeme commented

After going over this again and again, I am wondering, what exactly is the use-case for "abstract states"?...

Taken the example, why not just do all the necessary stuff on .state('contacts') for the url /contacts, e.g. defining all views. instead of having the contacts.list state?...

@nshahzad if you want to have the ability to switch between "compatible" layouts for the state, you could make 'template' or 'templateUrl' a function that switches between those layouts at runtime based on a user setting or a state parameter (the $stateParams object gets passed to the template or templateUrl function). This is probably easier than duplicating all your states.

@ksperling : "The ability to define state.url relative to the parent state's URL is simply a convenience provided by $stateProvider.state(), because it's very common thing to do (and there is an 'escape' mechanism of prefixing the URL with '^' to tell $sP.state() to treat it as absolute)."

This feature (absolute paths when starting with '^' symbol) is very useful (at least in my project). I was thinking that it was the default behavior, but it seems like it's not. I was looking for it in the documentation, but couldn't find it there. But luckily I was able to find this thread. It would be nice if wiki would be updated. In fact, I will try to do it myself if no one minds.

@kmalin I see that you modified the wiki. Thanks!

@jeme & @ksperling I'd like to talk more about abstract states as well. I'm not sure they are a user-friendly aspect of ui-router. I brought it up multiple times early on and @ksperling felt strongly about them and I've gone back and forth on how I feel. I've seen new users use them incorrectly and also be very confused about them. I'm getting the notion that we need to look at this feature more deeply.

What is its exact purpose that makes it unique and needed in ui-router? What problem is it solving??
Are there other ways to do the achieve the same outcome that an abstract state provides?
Is there a more layman term and solution?
How would a new person perceive an abstract state? How would they better understand what its for?

AFAIK they are used to DRY state definitions, when multiple states share things in common (like abstract classes in OO programming). Maybe you can think of a good way to approach this from the docs?

Maybe I find them intuitive because I think of states as being in some sense quite similar to classes in an OO language (I mostly do Java by day...)

@nateabele is right, they factor out commonalities.

As an example, say your app had 'clients' and a 'suppliers' parts with different top level nav. You could define an abstract 'clients' state with a template like this: <!-- client top level nav ... -> <div ui-view></div>, and each of the child states then put something meaningful into that ui-view. But the 'clients' state itself is abstract because the user would see a nav-bar only, with nothing in the ui-view.

Now you might say that you should put whatever the default child does into that abstract parent state and make it non-abstract, but that's not a good idea for two reasons:

  • it makes it hard to change your mind about which child is the default, because the parent has subsumed the default child
  • any other children suddenly inherit 'resolve' items from the default "child", and the enter/exit lifecycle differs etc...

I think the confusion comes from people thinking about the structure of their app too much in terms of URLs, and from the idea that if a URL like "/foo" maps to state FOO, and "/foo/bar" maps to state BAR, then BAR must be a child of FOO. It's as if each additional "/" means one more nesting level in the state tree. This is not the case!

Going with the sample app contacts.details and contacts.list states again, what's going on would probably be easier to understand if the URLs of those two pages were "/contacts/list" and "/contacts/details", and "/contacts" would be an invalid URL. But people (reasonably, given how things traditionally work on the web) expect /contacts to be the URL for the default child. If you were writing static HTML pages, you might have two files, "/contacts/index.html" and "/contacts/details.html". Note that they're both inside the "/contacts/" directory, index.html is on the same nesting level as any other child of contacts, the web server just happens to let you use a shorter URL to access it, so really it's not much of a 'default', it just happens to have a shorter URL.

jeme commented

@ksperling your example of "/contacts/index.html" and "/contacts/details.html" falls into the old days of the web, and people rarely use such structure in my experience any more... instead they would have "/contacts" and "/contacts/details", which would have to map to "/contacts/index.html" and "/contacts/details/index.html" in the traditional days...

Today we can decouple the URL from where the resource is actually located, it may not even be a file anywhere.

The thing is, "Abstract" doesn't really serve the explanation you put forth, that's the ability to define a default child that does that. All Abstract really does is to deny you the ability to go to that state...

And it's funny we keep saying "default child"... In my mind... there is no default child... there is a number of states and then there is the routes that map to them... so the actual mapping is just:

'/foo' = goto 'foo.default'
'/foo/bar' = goto 'foo.bar'
'/foo/baz' = goto 'foo.baz'

There is no url/routes that goes to foo.

And so I think we should turn the thing up side down when we explain it, and focus on what that "default" child does, and forget about "abstract" all together.

Abstract could go into it's own explanation on how to deny you the ability to activate a particular state. I think we will find that then, no one really ends up using abstract. (unless the state machine only understands a child route of `` if the parent is abstract, in which case this whole post is sort of invalid)

@jeme I agree that there is fundamentally no default child -- but I think some of the confusion people have with designing their state hierarchies come from assuming that there is... seems like my web 1.0 analogy isn't a good explanation either.

You don't HAVE to use abstract, but it lets you do two convenient things:

  • it lets you define a common URL prefix that the child states can inherit (if they use relative URLs) without having the presence of the URL causing a route to that state from being created
  • it explicitly prevents you from accidentally transitioning into a state that's not meant to work by itself. As a feature t's on the same level as marking something as "const" in C or "final" or "abstract" in Java -- some people prefer to type less and do these things by convention, other people prefer to make these things explicit and have the tool/framework catch the issue early.

Given the miniscule amount of code the implementation of this feature takes up, I think it's worthwhile to have it. I don't think it's a very important feature for beginner / getting started docs.

If you have a child route of '', the child and the parent will end up with the same route, so you'll only ever hit the parent state unless you transition via code (because the parent route gets added first and routes are evaluated in order).

Yeah, that's a good idea. Maybe just omit abstract states from the docs for beginners, and save it for a section with more advanced concepts (of which I'm sure there will be several).

jeme commented

If you have a child route of '', the child and the parent will end up with the same route, so you'll only ever hit the parent state unless you transition via code (because the parent route gets added first and routes are evaluated in order).

Hmmm.... So...

provider
  .route('/home', goto fubar)
  .route('/home', goto home);

Would go to Fubar i hear, that is opposite than what I do... for Angular-routing it would go to home...
That is why I ultimately only saw abstract as a way to block a state from being a target... and therefore less important. Which means that your (1) can be achieved without an abstract concept.

Ideally I would like a construct like that to throw at config time -- it's clearly nonsense to have two identical routes and probably a configuration error.

Maybe 'incomplete' would be a more descriptive name for the non-classical-OO crowd, or at least a term that can be used in the description of the feature.

jeme commented

Ideally I would like a construct like that to throw at config time -- it's clearly nonsense to have two identical routes and probably a configuration error.

That depends on what those lines means... In my case there is only one route... it is overwritten as we make the second call, just like in the original router. Which is what I have leaned against...

I didn't wan't to throw an error if people find the ability to reroute things during run-time useful... And since I now use that ability... well... :)

But that isn't what we are suppose to discuss here. Merely stated it to clarify why my views was as they was.

I'm still just feeling confused. Not in how abstract works (I get it and see the merit), but I get this feeling that there's more weight to the term than necessary.

Sounds like abstract states do two things:

  • provide info/data to inherit to child states
  • disallow navigation to self

It seems like we could achieve the same results but make it more user-friendly if we renamed abstract:true to navigable: false. Since all states inherit by default that is not something special of an abstract state. What is special is that you don't navigate to it. By using the term 'abstract' which is a top-tier OOP term, we are just limiting the features reach.

What do you think about them apples?

But remember, navigable isn't a flag. ;-) I think your explanation is fine, but I also think abstract is a simple, straightforward term that represents a concept which should be familiar to most developers.

@timkindberg 'navigable' is internal and not part of the public API, plus it only refers to navigation via URL, as opposed to the state being able to be activated at all.

I agree with @nateabele that 'abstract' should be familiar for most developers. It means the entity in question "defines stuff but you can't use it directly, you can only inherit from it"

Alright I'll give in again, you guys drive a hard bargain :).

Though just to clarify my suggestion was to not use the existing navigable internal property, but create a new one for the state config object because the name worked well, it just happened to be the same name as the internal property.

I was just reading through this discussion, and was wondering if you had any input on my situation.

I have a register form which is used to register new users based on invitations by an existing user. The new users get a /user/get-started/[hash] url which they click to get started.

My thinking was to keep the same URL throughout the whole register process, and only change the templates.

Currently I have four states:

'getStarted'
url: '/user/get-started/:hash'
controller: 'RegisterCtrl'
'getStarted.validateHash'
template: ...
'getStarted.enterInfo'
template: ...
'getStarted.registrationComplete'
template: ...

When the user navigates to the URL I would like to go to state getStarted.validateHash, but I cannot make it leave the getStarted state. I have tried setting getStarted to abstract, or putting a template on it (and then transitioning from the controller), but it seems like I just get into the getStarted state and then nothing.

I have read back and forth through the documentation, but have not found a way of getting to my sub state.

Any ideas would be appreciated.

Just assign your url directly to the validateHash child state, and then transition between the siblings using $state.transitionTo()

I would like to second @timkindberg's original request to support default child states. The use case is simple. Consider the 'contacts' state which may have three child states 'contacts.list', 'contacts.new', 'contacts.edit'. Other parts of the program just want to transition to 'contacts' without needing to know which child state is the default. Its the job of the router to know that 'contacts.list' is the default landing state for contacts. Maybe in the future a new child 'contacts.favorites' will be implemented and become the new default. Nobody but the contacts routing logic should know this. As it stands right now, I must either navigate by url instead of state name, or use a constant to define the default state for contacts, or some similar semi-ugly solution.

Other parts of the program just want to transition to 'contacts' without needing to know which child state is the default. Its the job of the router to know that 'contacts.list' is the default landing state for contacts.

I think this is the single most important use-case for supporting default child states. Following @kbaltrinic's example, with existing behavior you're essentially forced to manually find and update all relevant instances of ui-sref="contacts.list" to contacts.favorites in your templates. This is a lot of work for what should be a one line change.

The original proposal seems pretty elegant to me, but I'd suggest one minor addition:

if no default child state is set, first child state found will be used as default

Having the ability to set default: false on the abstract state to prevent the automatic promotion of a default child (i.e. preserve existing behavior).

I didn't realize #1235 is a duplicate of this.

My solution is to simply do abstract: 'child.state.name' or abstract: function(){ return 'child.state.name' }

I do not think there should be any 'automatic' selection of a child. You either can't navigate to an abstract, or if you do, a default MUST be supplied in the state definition.

sricc commented

I agree with @kbaltrinic and @timkindberg's original request! I have hit this exact use case with list, add and edit. +1

I'm in favor of @ProLoser's proposed implementation. We can roll that in once Transition objects are fully integrated (that way we can do things like "abstract": ($transition) => $transition.from === fooState ? "this.child.foo" : "this.child.bar").

@nateabele Hopefully we can support returning a string and (state, param) objects like the following:

.state('Account.Security', {
          redirectTo: 'Account.Security.ChangePassword', // OR
          redirectTo: { state: 'Account.Security.ChangePassword', params: { id: 123} }, // OR
          redirectTo: ['$state', 'authenticationService', function($state, authenticationService){ 
                //Some logic
                return 'Account.Security.ChangePassword'; // OR
                return { state: 'Account.Security.ChangePassword', params: { id: 123} };
            }]
        });

Personally I prefer redirectTo over abstract since it better explains what is happening.

@acollard yes and no. I feel it makes sense of you read it as "on entering this state directly" but it's not as intuitive when you think about going directly to a child state.

I would prefer documenting it as "the abstract key can be true or you can give it a redirectTo string or function"

The redirectTo concept is definitely more general, especially if you think about it as a way of having one state encapsulate logic to dispatch to several other states. I'll have to think about it.

There are two things which would be nice to have out-of-the-box.

  1. A start state, so if you have a abstract state super which has two child states, child1 and child2 and marking child1 as default, then transitioning to super would automatically transition to child1.
  2. A history state, so if you exit super from child2, transitioning to super again would automatically go to the child state that was previously active, i.e. child2.

It seems like both of these things are possible using the redirectTo functionality suggested by @acollard, but it would be nice to be able to do it explicitly.

I think allowing you to create a redirect callback will allow you to do the
"last state" stuff yourself because I have worked with a lot of
implementations and I think it's outside the scope of this issue
On Apr 28, 2015 8:33 AM, "Jonas Rabbe" notifications@github.com wrote:

There are two things which would be nice to have out-of-the-box.

  1. A start state, so if you have a abstract state super which has two
    child states, child1 and child2 and marking child1 as default, then
    transitioning to super would automatically transition to child1.
  2. A history state, so if you exit super from child2, transitioning to
    super again would automatically go to the child state that was
    previously active, i.e. child2.

It seems like both of these things are possible using the redirectTo
functionality suggested by @acollard https://github.com/acollard, but
it would be nice to be able to do it explicitly.

โ€”
Reply to this email directly or view it on GitHub
#27 (comment).

Probably so, but currently history management is weak bordering on non-existent, so that particular use case is part of a larger set of things that need to be considered.

I think redirectTo might be useful for non-abstract states, too (during refactoring of state names, it might be useful to redirect the old state name to a new state name), but is really a more general case.

FWIW, it's pretty easy to add basic support for a default child state configuration by decorating the $stateProvider (see angular-ui-router-default). Adding support for specifying a {state, params} configuration would be easy as well.

BTW, one might also consider specifying the default child state configuration by assigning a string value to the abstractsetting instead of using a default property, e.g.:

.state({
  abstract: '.defaultChild'
})

God I am going to have to review this thoroughly because I don't like the component router

I don't like the component router

We've been getting a lot of that.

Is there an elegant version of this: #948 (comment) that includes using resolved data? I'm racking my brain trying to come up with a way on the current router to have a default child based on resolved values. I'm getting a bit of redirect-loop-complexity with my old implementation thus far.

I do like to think that something like the following could be implemented, native to UI-Router:
https://github.com/nonplus/angular-ui-router-default

@christopherthielen: sorry to comment on this old post, but I'm thinking this answer is just what I need. When I try your code though, $state is always null, so the $state.target(val) results in an error.

Can you maybe give me a hint what to look for in such a case?