when to use child states vs multiple views with ui-router - angularjs

Last hours I have been reading through docs of UI-Router. But I can't find a solution for my problem.
My webapp has two different columns, a list on the left and a detail view on the right. Selecting a element of the list should show detail information on the right.
Which of this two approaches described in the title would you prefer? When to use what?

In fact, the List x Detail scenario is the most suitable for ui-router. These are in fact two states, the parent/child (i.e. child states to answer the question):
a List view (e.g. the left column). This could be a dynamic view, with paging, sorting and filtering, but still - this will always be a gateway, a parent to:
a Detail view (e.g. the right column). To select a detail (unless navigating via url directly) we simply need a List view. To select different detail, we can profit from a fact, that the parent/List view state is not reloading, while iterating among many details...
The best we can do is to observe the example, provided by ui-router team:
http://angular-ui.github.io/ui-router/sample/#/contacts
And we can also see its definition, which is part of this states definition:
https://github.com/angular-ui/ui-router/blob/master/sample/app/contacts/contacts.js
this link, belongs to the best documented pieces of code I do remember... It explains everything and also helps to learn how the ui-router state definition is working.
Below I tried to show that power by citing the definition of the List and Detail states.
The List state:
/////////////////////
// Contacts > List //
/////////////////////
// Using a '.' within a state name declares a child within a parent.
// So you have a new state 'list' within the parent 'contacts' state.
.state('contacts.list', {
// Using an empty url means that this child state will become active
// when its parent's url is navigated to. Urls of child states are
// automatically appended to the urls of their parent. So this state's
// url is '/contacts' (because '/contacts' + '').
url: '',
// IMPORTANT: Now we have a state that is not a top level state. Its
// template will be inserted into the ui-view within this state's
// parent's template; so the ui-view within contacts.html. This is the
// most important thing to remember about templates.
templateUrl: 'app/contacts/contacts.list.html'
})
the Detail state:
///////////////////////
// Contacts > Detail //
///////////////////////
// You can have unlimited children within a state. Here is a second child
// state within the 'contacts' parent state.
.state('contacts.detail', {
// Urls can have parameters. They can be specified like :param or {param}.
// If {} is used, then you can also specify a regex pattern that the param
// must match. The regex is written after a colon (:). Note: Don't use capture
// groups in your regex patterns, because the whole regex is wrapped again
// behind the scenes. Our pattern below will only match numbers with a length
// between 1 and 4.
// Since this state is also a child of 'contacts' its url is appended as well.
// So its url will end up being '/contacts/{contactId:[0-9]{1,8}}'. When the
// url becomes something like '/contacts/42' then this state becomes active
// and the $stateParams object becomes { contactId: 42 }.
url: '/{contactId:[0-9]{1,4}}',
// If there is more than a single ui-view in the parent template, or you would
// like to target a ui-view from even higher up the state tree, you can use the
// views object to configure multiple views. Each view can get its own template,
// controller, and resolve data.
// View names can be relative or absolute. Relative view names do not use an '#'
// symbol. They always refer to views within this state's parent template.
// Absolute view names use a '#' symbol to distinguish the view and the state.
// So 'foo#bar' means the ui-view named 'foo' within the 'bar' state's template.
views: {
// So this one is targeting the unnamed view within the parent state's template.
'': {
templateUrl: 'app/contacts/contacts.detail.html',
controller: ['$scope', '$stateParams', 'utils',
function ( $scope, $stateParams, utils) {
$scope.contact = utils.findById($scope.contacts, $stateParams.contactId);
}]
},
// This one is targeting the ui-view="hint" within the unnamed root, aka index.html.
// This shows off how you could populate *any* view within *any* ancestor state.
'hint#': {
template: 'This is contacts.detail populating the "hint" ui-view'
},
// This one is targeting the ui-view="menu" within the parent state's template.
'menuTip': {
// templateProvider is the final method for supplying a template.
// There is: template, templateUrl, and templateProvider.
templateProvider: ['$stateParams',
function ( $stateParams) {
// This is just to demonstrate that $stateParams injection works for templateProvider.
// $stateParams are the parameters for the new state we're transitioning to, even
// though the global '$stateParams' has not been updated yet.
return '<hr><small class="muted">Contact ID: ' + $stateParams.contactId + '</small>';
}]
}
}
})
Summary: In these scenarios, do use the parent/child state definition, because the parent will be loaded only once, and keep its data, while we are iterating among its children
Check these links for some more details:
Angular UI Router Nested State resolve in child states
why $routeChangeSuccess never gets called?
How do I prevent reload on named view, when state changes? AngularJS UI-Router

Related

Angular UI-Router: How to call child state without its parent?

We have recently move our application to Ui-Router from ng-route. And we are trying to refactor some points in our code regarding routing.
We have a list view and a detail view in our application. The detail view will be shown as a pop-up on top of the list view, so in order not to initialize all list controller logic again and again, we have defined our detail view as a child view-state. It looks like this:
$stateProvider
.state('list',
{
url: "/",
template: '<list-directive></list-directive>'
})
.state('list.detail',
{
url: "/detail/{item_id}",
template: '<detail-directive></detail-directive>'
})
It actually works as expected. When i open a detail view from a list view, the list view (i mean the controller) does not run again, and when i close the detail view, the list view remains.
But now we would also like to call the detail view DIRECTLY, without revoking the parent. Currently when i directly call the detail state, the parent controller runs also.. How can i achieve this?
Is a parent - child relationship not a appropriate one for our scenario?
According ui-router doc :
Child states can be used to drill down from a more general feature to
more specific one, or to implement a master/detail pattern. (...) The child state’s view usually renders inside a viewport that the parent state created. This is referred to as Nested Views (...) These substates share the common parent state contacts and inherit data and behavior from the parent.
So I think you should use it if you don't want these behaviour .
It did work like this:
$stateProvider
.state('list',
{
url: "/",
template: '<list-directive></list-directive>'
})
.state('detail',
{
url: "/detail/{item_id}",
template: '<detail-directive></detail-directive>'
})
.state('list.detail',
{
url: "detail/{item_id}",
template: '<detail-directive></detail-directive>'
})
But the angular is very fragile.. and behaves very different with very small changes.. the slashes are the problem.
Child state list.detail does not have a leading slash in its URL. But our new state detail (which has exctly the same content as the list.detail) should have a leading slash so it can be called directly. So We have the same state both as child and as parent.
Now when we call directly the URL "host/detail/5" we go to detail view without revoking the parent. And we we call ui-sref=".detail(5)" we go to child state (list.detail) within the parent

Using UI-Router to split up parts of a big data entry form

I have a big data entry form that's defined programmatically.
I'm modeling it with a PlanController.
On launch, the PlanController loads the plan, its sections, and fields.
The PlanController's template shows:
the plan header information
a navbar loaded from the plan's sections, like this:
li.nav-item(ng-repeat="section in plan.sections | filter:{'active': '1'}" ui-sref-active="active")
a.nav-link(ui-sref="plan.section({idSection: section.idSectionDef})" ) {{sexion.title}}
This is all working great.
Note: Since I don't know what sections there are beforehand, or what the fields in them are, I decided to use a ui-router child route to display the information section information.
This where I run into problems
All the Plan information is loaded in the parent Controller. I'm not sure how to bind the child information in the child view to the parent to the parent.
First Try - no child controller
I've tried declare the child (section) state without a controller, and try to access the section information directly from the PlanController:
.state('plan.section', {
url: '/section/{idSection}',
templateUrl: 'app/plan/planSection.html'
})
but in binding the section fields in the child state's template, planSection.html :
div.col-md-4.col-sm-12
ilp-field(ng-repeat="fld in getFields($stateParams.idSection)| filter:{ layoutColumn: 10, idSectionDef: $stateParams.idSection}" datafield="fld" )
div.col-md-8.col-sm-12
ilp-field(ng-repeat="fld in getFields($stateParams.idSection) | filter:{ layoutColumn: 20}" datafield="fld" onchangecallback="checkilp")
I get the fields for the first section ($scope.getFields takes a section id and returns a list of fields that belong to that section).
When I select one of the other child sections, the url is showing the change of states, but the fields are not changing.
Second Try - child controller
I originally tried declaring the child state with its own controller:
.state('plan.section', {
url: '/section/{idSection}',
templateUrl: 'app/plan/planSection.html',
controller: 'planSectionController'
})
but I don't know how to get the list of fields from the parent to the child control, and when I try to bind to:
$state.$current.parent
as in
ilp-field(ng-repeat="fld in $state.$current.parent.getFields($stateParams.idSection)| filter:{ layoutColumn: 10, idSectionDef: $stateParams.idSection}" datafield="fld" )
I get an error that $state.$current.parent is not accessible.
Would welcome some guidance on the right design pattern to use here with ui-router

ui-router renders template before state changes

I have a nested state structure like this:
$stateProvider
.state('main', {
url: '',
abstract: true,
templateUrl: ...
controller: mainController,
resolve: {
Resolve1: ...
Resolve2: ...
}
})
.state('main.state1', {
url: '^/state1/:id/',
templateUrl: ....
controller: state1Controller,
resolve: {
Resolve11: ...
Resolve22: ...
},
})
.state('main.state2', {
....
From what I am seeing, when you are at state main.state1 and you navigate to the same state with another id parameter, the template for main.state1 is rendered fine but the template for main (the parent) is rendered before the state changes (I guess it doesn't wait for the data resolution of main.state1). This results to the view being rendered with the wrong data (more specifically, the state parameters that I use for generating links are wrong).
My mainController has the $stateParams and $state injected to it and that's where I get the data from.
Did anyone notice this before? Is this by design or is it a bug?
Is there any way to update the parent view with the latest data?
However, I would expect ui-router to wait for all data resolutions until it starts rendering the views (even the parent). I don't know if I am missing something here, but this is my understanding of the problem so far...
UPDATE:
I see now that a function that is involved in the view rendering (with interpolation) gets called many times, first with the old values and then with the new values. But the final result that I see on screen is using the initial data that were used when I first entered the 'main.*' state. This is SO weird.
UPDATE 2:
I have found that my links where not updated ONLY when using ui-sref (note that I was using parameters in the state href, e.g. ui-sref="main.state1({id:{{getId()}}})"). When switched to ng-href everything worked as expected.
I have no idea why, but that fixed the problem.
The odd thing was that the ui-sref was evaluated, but the link was not updated.
Go figure...
UPDATE 3:
This solution in turn caused other problems. Then the first click reloaded the application... So this is not the best fix...
The "solution" was to use ng-click with $state.go(), instead of links.
This appears to be a bug in ui-router.
ui-router transitions "through" the parent state to the child. That means main will be triggered, then main.state1. I ran into this myself where I was getting duplicate listeners and HTML DOM elements - main is actually running AT THE SAME TIME as main.state1. This behavior is by design, and AFAIK your only option is to design around it.

# and named# doesn't work on the same state views with resolve

I have a state named 'index' that, depending on the ui-view on the page, display different information and have different templateUrl and share the same URL:
$stateProvider.state('index', {
url: '/',
views: {
'all#': {
templateUrl: '/templates/partials/all/index',
controller: function(){
console.log('main');
}
},
'#': {
templateUrl: '/templates/partials/home/index',
controller: Controllers.Index,
controllerAs: 'academia',
resolve: {
templates: Preload.go(),
academia: ['Info', (Academia: Academia.Services.Info) => {
return Academia.get();
}],
plans: ['Planner', (Planner: ngPlanner.Planner) => {
return Planner.init();
}]
}
}
}
});
when the page is loaded with a <div ui-view></div>, it works as intended (matching the '#' rule). but when the page is loaded with <div ui-view="all"></div> it doesn't.
A plunkr showing the problem http://plnkr.co/edit/a2SWbB?p=preview
The resolve from the unnamed # shouldn't interfere with the all# view. The current resolve is being rejected because it doesn't belong on the current view (since there's no unnamed view when the page is loaded). Is it a bug, or am I doing something wrong?
At these days, the UI-Router implementation is driven by state management. And despite of the fact, that there are some voices/requirements to split the resolve of the views (well, not the parent view vs child view - but that is discussed below) ... I would say, current behavior in the Q&A described above is simply correct.
Firstly, what is the problem in above question? In general there are two or more sibling views, with their own resolve definitions:
views: {
'view1': {
....
resolve : { // could fail
}
},
'view2': {
...
resolve: { // could fail as well
...
What is state?
A state is a cluster of settings, representing the state. (sounds like self proving statement, but I am serious). All the settings are about state definition, about one, specific state.
We can use more views to represent it (e.g. title and content).
We can use some resolve statements (for all sibling views, for some of them) to support these views.
We have still only ONE state.
And that's it. It is all or nothing. The state is one, one cluster of settings, which all of them must be workign/resolved - or the whole state is failing.
Another story: parent - child
There are also some requirements to let the parent view to render once all its stuff is resolved. Why? Currently, if we navigate directly to child state, all/both resolves must be resolved before the state is initiated, the views are rendered.
That could be really improved. Make sense. And as far as I remember it is planned to happen in later releases (I'would swear I've seen that in one of the comments by UI-Router team member - but cannot find it).
It is different scenario. Because 1) we can go to parent (list view) first. And then 2) select child (detail). In this scenario it could really happen that list is working, detail fails.
So going directly to child should render parent, next fail on a child... But this is different story then the sibling described above.
Suggestions:
My suggestion would be, keep in resolve only stuff which is essential, absolute MUST for a state. Move the rest into Controllers injected services. This is my suggestion and my way...

why angular-ui new state router transitions when only parameters changes?

I'm using the new angular-ui router, the one that has a state-machine included (https://github.com/angular-ui/ui-router). This great router allows a user to specify parameters as part of the URL.
for example:
$stateProvider
.state('contacts.detail', {
url: "/contacts/:contactId",
templateUrl: 'contacts.detail.html',
controller: function ($stateParams) {
// If we got here from a url of /contacts/42
expect($stateParams).toBe({contactId: 42});
}]
})
(see here)
this means, that when the user navigates to /contacts/42, the state is changed to 'contacts.details' and the 42 parameter is injected into the controller
There is a problem though. If ONLY the url parameter changes, the transitionTo function is still being called (could happen if the url is changed manually, for example, or bound to a input box). this in turn leads to the view directive of that state, to be re-created, both a waste of time and a problem if we only wanted to update something in that state.
it seems to be on purpose. from the code:
// Starting from the root of the path, keep all levels that haven't changed
var keep, state, locals = root.locals, toLocals = [];
for (keep = 0, state = toPath[keep];
state && state === fromPath[keep] && equalForKeys(toParams, fromParams, state.ownParams);
keep++, state = toPath[keep]) {
locals = toLocals[keep] = state.locals;
}
equalForKeys is what compares the params, and return false if there's a difference.
My question: do you know why the author would have written it this way? do you think its safe to change, so that there's no transition when only parameters would change?
thanks very much for reading all the way till here, and for any idea
Lior
EDIT: Seems that this is by design. just found: https://github.com/angular-ui/ui-router/issues/46
I generally solve problems of this nature with abstract states and nesting. Place the pieces that don't change based on the url parameter into the abstract parent state and you'll avoid the extra server hit. I prefer to place them into the resolve or custom data section in the parent state, but if necessary you can retrieve them via scope inheritance. Be sure you read and understand the rules of scope inheritance as there are some things that (at least for me) were unexpected.
You can read more details here: https://github.com/angular-ui/ui-router/wiki/Nested-States-%26-Nested-Views. In addition, the sample application included with angular-ui is a good place to start understanding state nesting.

Resources