I feel a bit silly for asking this, I've looked and searched for this for quite a few hours and know the two main methods for setting up default child states is to:
url: '',
or
$urlRouterProvider.when('/route', '/newroute');
I'm trying to use the empty url config as the reroute option causes unwanted issues when the parent state is set to abstract: true. All templates contain a ui-view element(s).
My code looks similar to this at the moment:
.state('data', {
url: "/data",
templateUrl: "views/data.php",
})
.state('data.overview', {
url: "",
templateUrl: "views/subviews/data.overview.php",
})
.state('data.overview.view', {
url: "",
templateUrl: "views/subviews/subviews/data.overview.view.php"
})
.state('data.overview.view.msgfav', {
url: "",
views: {
msg: {
templateUrl: "views/subviews/subviews/data.overview.view.msg.php"
},
fav: {
templateUrl: "views/subviews/subviews/data.overview.view.fav.php"
}
}
})
As well as some more child states (which didn't seem relevant to include since not meant to be default). I've tried with the parent state as abstract: true and without setting it, with url empty and no url setting at all, with only the closest child state and not all nested. Sticking an ng-include in there can work as default state, but it feels unclean.Targeting each child directly works fine. I'm really not sure what I'm missing here, but I'm sure it's some small detail that will make me look silly.
I'm not sure whether I should reply with an answer to my own question, but think that's appropriate.
I don't think you can target the parent state (abstract) directly in ui-sref and expect it to default to child state and populate the view, but rather target the child state in links. Then have to set every parent state down the chain to abstract down to the point you want populated automatically. I didn't find this completely clear, and one part of the answer in this post threw me off a little on that;
"First, add a property to the 'manager.staffDetail.view' state of abstract:true. This isn't required, but you want to set this since you'd never go to this state directly, you'd always go to one of it's child states."
angularjs ui-router default child state
Related
I have define state for my shop application but I'm not sure I'm doing it right. Since I have more than one optional parameter in url I'm not sure how should I implement it.
.state('app.products', {
abstract: true,
views: {
'content#': {
templateUrl: 'app/products/views/product.html'
},
'productHeader#app.products': {
templateUrl: 'app/products/views/product-header.html'
}
}
})
Above is my abstract view for products page. Products will be separated in man/women and also subcategories like:
www.example.com/man/
www.example.com/man/footwear/
www.example.com/man/footwear/shoes
Man, footwear and shoes are all optional since man param can be woman, footwear can be cloth (where last param would be e.g. shirts) and all possible combinations of those above.
I'm not sure if I have to make every state separately or I can handle all this with one more state except this one?
Just to note, product header is not relevant here and if its required for good structure to remove it, surely I can do that.
I just can't find anything similar online, so link would be also be helpful if anyone has any.
I've done something very similar recently by nesting each subcategory state into its parent category state. Some benefits of doing it this way are that you save yourself from having to repeat a lot of code in a child state that was already defined in a parent state, and also from having to reload data and views that were already loaded in the parent state.
Here's an example to get you started:
.state('app.products', {
abstract: true,
url: '/products',
views: {...}
})
.state('app.products.gender', {
url: '/:gender',
views: {...}
})
.state('app.products.gender.category', {
url: '/:category',
views: {...}
})
.state('app.products.gender.category.type', {
url: '/:type',
views: {...}
})
First, the urls automatically stack in child states. This means you only have to define one url parameter per child state and you still wind up getting urls like this /app/products/:gender/:category/:type.
The second benefit of doing it this way is that views defined in a parent state are automatically included in all of its child states, unless you explicitly override it:
.state('app.products.gender.category', {
url: '/:category',
views: {
'foo#someParentState': {templateUrl: 'foo.html'},
'bar#someParentState': {templateUrl: 'bar.html'}
}
})
.state('app.products.gender.category.type', {
url: '/:type',
views: {
// foo.html is still rendered here
// bar.html is replaced by baz.html
'bar#someParentState': {templateUrl: 'baz.html'}
}
})
Another benefit seen from this example, is that foo.html won't be reloaded when the state changes to app.products.gender.category.type. For example, say foo.html has a long scrolling list of types in that category. If the user clicks on an item in the list that changes the state from app.products.gender.category to the child state app.products.gender.category.type, then foo's long scrolling list will not be reloaded, and the user can still see the item they clicked on. On the other hand, if that click had changed the state to a non-child state, then the list would probably have been reloaded (data and all), and the user might have to scroll to see the item they clicked on.
Some advice:
Keep your nested state names short.
Only include a state in the hierarchy if it's absolutely necessary (I'm looking at you app.products!).
There are plenty of ways that this technique can go wrong so be sure to review the ui-router docs for configurations that help you code less.
I'm still working on an angular app, using the great ui-router. I'd like to use some dynamic nested states (I talked about it here), so I know it's possible.
Here is my code, with a specific state and its dynamic children states :
.state('home', {
url: '/home',
controller: 'RouteCtrl'
})
.state('home.state', {
url: '/home/:state',
controller: 'RouteCtrl'
})
$urlRouterProvider.otherwise('/home')
I have 3 buttons (more specifically, I use <a> to make it) to access 3 differents states : 'home', 'contact' and 'about'. 'contact' and 'about' are 'home' nested states and every state has a specific text to display when activated.
Unfortunatly, it appears that both of the children states aren't resolved from 'home' state.
Here is a plunker of the problem which match with my problem. Any idea ?
This will give you a working call to the new states:
$scope.redirect = function(state) {
console.log('redirect to state : ' + state);
if (state != 'home') {
$state.go('home.state', {
'state': state
});
} else {
$state.go('home');
}
}
However, it still won't change the text on the page, because the controller only sets it once when initially loaded.
Technically home, contact and about are not 3 states. What you appear to be doing is altering the content based of the parameters of the state. This could be achieved using one state and forcing ui-router to reload the state when you use $state.go
I have modified your plunkr here http://plnkr.co/edit/XXaltjG17FwY15tSbKaD?p=preview
Your state definition could look something like this,
.state('home', {
url: '/home?state',
controller: 'RouteCtrl'
})
The question mark makes the state parameter optional and also a query string.
The redirection could look something like this. You need to reload as you are going to the same route
$state.go('home', {state: state}, {reload: true});
Redirecting to the home page could look something like this. You need to disable inheritance for this redirect as you don't want to keep the query strings.
$state.go('home',{}, {reload: true, inherit: false});
The main problem here is that you want to have a variable in the state. You can't go to state home.about since it's not a given .state.
You should look at stateParams, or you can specify the URL where you want to go to the URL with Angular's $location.
Note: I think the url for a child state like home.state does not need the /home URL since it's in the father's state.
When using onEnter to redirect to a state, if the new state is a child of the current state, an infinite loop occurs.
Example:
$stateProvider
.state 'inventory',
url: '/inventory'
templateUrl: 'views/inventory.html'
controller: 'InventoryCtrl'
onEnter: () ->
$state.go 'inventory.low'
.state 'inventory.low',
url: '/low'
templateUrl: 'views/inventory-table.html'
controller: 'LowInventoryCtrl'
When:
$state.go 'inventory.low'
Is called, the state inventory is re-initialized, causing it to be called again = infinite loop.
However, if the redirect state is:
$state.go 'otherStateThatIsNotAChild'
This issue does not occur. I assume that the parent state is being re-initialized, but why?
Why is the parent state being reinitialized when .go is called on a child state?
How then, would you handle redirecting to a child state?
1) Why is the parent state being reinitialized when .go is called on a
child state?
While a transition is in process, any $state.go/transitionTo will cause the currently in process transition to be Superseded. An in-process transition that is superseded is cancelled immediately. Since your original transition to inventory is not completed by the time all the states' onEnters are called, the original transition is cancelled and a new transition to inventory.low is started from the previously active state.
See ui-router src https://github.com/angular-ui/ui-router/blob/master/src/state.js#L897 ...
2) How then, would you handle redirecting to a child state?
You could...
Wrap $state.go in a $timeout() to allow the original transition to complete before redirecting.
Call $state.go from your controller instead. UI-router invokes the controller from the ui-view directive AFTER the transition is completed.
In any case, be very sure you want your app to redirect like this. If a user navigated directly to any other child state of inventory (such as inventory.high), the redirect will still occur, forcing them to inventory.low which would not be what they intended.
I had the same problem. The simple solution I found is to listen state changes and redirect to your child state from there.
It's kind of hack that makes routes redirecting to child states by default. It's not needed to do url: '' or $state.go(), they don't work correctly.
So, in config file:
$rootScope.$on('$stateChangeStart', function(event, toState, toParams, fromState) {
if (toState.redirectTo) {
event.preventDefault();
$state.go(toState.redirectTo, toParams);
}
});
In state file:
.state('home', {
url: '',
redirectTo: 'home.list'
});
I've made an example on gist: https://gist.github.com/nikoloza/4ab3a94a3c6511bb1dac
You need to step back and think about what you're trying to achieve. What's the point of having a state when all it's doing is redirecting to a child state?
Regarding your first question, parent states are always activated when you arrive at a child state, this behaviour is extremely useful in sharing data among states, and without it nested routing would be impossible (or rather wouldn't make sense).
As for the 2nd question, I've worked on a few big angular apps and so far I haven't found myself needing to do that.
OK, believe it or not, as much I'd hate to say it, right now I came across a scenario where I needed to do this. I have a profile/userName route (technically this should be profile/userName/details) and a profile/userName/products route, I wanted to have a master view for both states but at the same time I wanted the profile/userName route have a clean url, like: profile/mike62, NOT profile/mike62/details. So I ended up doing this:
.state('publicProfile', { url: '/profile/{username}'})//this is the base/shell state
.state('publicProfile.details',{url:null})//I want this to be my de-facto state, with a clean URL, hence the null url
.state('publicProfile.products', {url:'/products/{exceptProductId:/?.*}'})
Ended up achieving it like this, there are many ways though:
in my publicProfile state controller (this is the base state):
$scope.state = $state;
$scope.$watch('state.current', function(v) {
if(v.name=='publicProfile') {//want to navigate to the 'preferred' state if not going to publicProfile.products
$state.go('publicProfile.details');
};
}, true);
Yes, it does feel hacky but now I know that there are some edge cases where we want to do this, althopugh we could rethink our state design altogether. Another, dirtier way would be to check the current state in a $timeout with a small delay inside the base state, if we are not on the publicProfile.products state, we navigate to our preferred/de-facto state of publicProfile.details.
It looks like you're trying to set a default child state.
That's a commonly asked question about ui-router: How to: Set up a default/index child state
The tl;dr is to use abstract states by settings abstract: true in the parent state. If you add multiple child states, it'll default to first child state.
$stateProvider
.state('inventory', {
abstract: true,
url: '/inventory',
templateUrl: 'views/inventory.html',
controller: 'InventoryCtrl'
}).
.state('inventory.low', {
url: '/low',
templateUrl: 'views/inventory-table.html',
controller: 'LowInventoryCtrl'
});
Per pdvorchik's answer on the related GitHub thread, there is a simple fix for this which worked perfectly for me, consisting of wrapping the $state.go call in a .finally() call to make sure the first transition completes before a new one is started.
onEnter: ['$state', function($state){
if ($state.transition) {
$state.transition.finally(function() {
$state.go('home.my.other.state', {})
});
}
}]
After #Chris T's explanation, it seems that the cleanest solution is to listen for the $stateChangeSuccess event:
redirects =
inventory: 'inventory.low'
$rootScope.$on '$stateChangeSuccess', (e, toState) ->
redirect = redirects[toState.name]
$state.go redirect if redirect
Where redirects will contain any route redirects that may occur. Thanks all!
Don't make the low inventory state a child state.
$stateProvider
.state 'inventory',
url: '/inventory'
templateUrl: 'views/inventory.html'
controller: 'InventoryCtrl'
onEnter: () ->
$state.go 'lowinventory'
.state 'lowinventory',
url: '/inventory/low'
templateUrl: 'views/inventory-table.html'
controller: 'LowInventoryCtrl'
The multiple nested views functionality of the ui-router is very nice - you can easily jump from one state of your app to another.
Sometimes you might want to change the URL, but sometimes not. I feel like the concept of state should be separate/optional from routing.
Here's a plunker that shows what I mean. This is a fork of one of the plunkers in the ui-router documentation, with 2 minor changes noted below:
.state('route1', {
url: "/route", // <---- URL IS SHARED WITH ROUTE2
views: {
"viewA": {
template: "route1.viewA"
},
"viewB": {
template: "route1.viewB"
}
}
})
.state('route2', {
url: "/route", // <---- URL IS SHARED WITH ROUTE1
views: {
"viewA": {
template: "route2.viewA"
},
"viewB": {
template: "route2.viewB"
}
}
})
This seems to work - the URL stays the same. Again, how much redundant work is done here? Is this an approved/tested usage?
It would be nice if you could omit the url from a state..
UPDATE: You can omit a url from a state. plunker
Update question: Is this an approved/tested usage?
You can absolutely have a state without a URL. In fact, none of your states need URLs. That's a core part of the design. Having said that, I wouldn't do what you did above.
If you want two states to have the same URL, create an abstract parent state, assign a URL to it, and make the two states children of it (with no URL for either one).
To add to the other answer, Multiple Named Views do not use a URL.
From the docs:
If you define a views object, your state's templateUrl, template and
templateProvider will be ignored. So in the case that you need a
parent layout of these views, you can define an abstract state that
contains a template, and a child state under the layout state that
contains the 'views' object.
The reason for using named views is so that you can have more than one ui-view per template or in other words multiple views inside a single state. This way,
you can change the parts of your site using your routing even if the URL does not change and you can also reuse data in different templates because it's a
component with it's own controller and view.
See Angular Routing using ui-router for an in-depth explanation with examples.
This is going to be difficult to explain but I'll try.
I am using UI-router in an angular app and would like t use the following URLS:
/contacts
/contacts/{id}
When you visit the /contacts page it will get a list of contacts from the server and display them. When you go to /contacts/1 it will get the contact 1 record from the server and show it.
My code currently looks like this:
.state('contacts', {
url: "/contacts",
templateUrl: "templates/contacts.tpl.html",
controller: "ContactsCtrl"
})
.state('contacts.contact', {
url: "/{contactID}",
templateUrl: "templates/contact.tpl.html",
controller: "ContactCtrl"
})
So far so good. but when you go to the second URL the parent is also activated so it's going to the server to get the list of contacts, even though they're not displayed, which is a waste.
I could set /contacts to "abstract:true" and use /contacts/list as the first URL, but that's not the URL I want to use and I do need to set a controller on the parent because I do have some logic I want to put in the parent (creating the navigation for that section).
Ideally, when the user hits /contacts I'd like the parent state to activate (to create the navigation) and run a default child state to list the contacts without redirecting to another URL. If the user goes to /contacts/8 then It would still activate the parent state but not the default state so it never goes to the server to get the contacts.
I hope that makes sense. I've not been able to create a plunkr, but the Angular UI guys kindly created one which shows the imperfect solution above.
http://plnkr.co/edit/gmtcE2?p=preview
I could set /contacts to "abstract:true"
That would be one part of the correct approach. A parent state should not load data that doesn't apply to a child, but your state tree doesn't have to reflect your URL structure exactly. For example:
.state('contacts', {
abstract: true,
url: "/contacts",
/* Various other settings common to both child states */
})
.state('contacts.list', {
url: "", // Note the empty URL
templateUrl: "templates/contacts.tpl.html",
controller: "ContactsCtrl"
})
.state('contacts.item', {
url: "/{id}",
templateUrl: "templates/contact.tpl.html",
controller: "ContactCtrl"
})