Angular ui-router: nested views vs multiple views - angularjs

ui-router is a great alternative to angular's standard router; it supports nested states and views and multiple views.
I am a little confused, though, by the difference between the two. It seems to me that multiple views can almost always be thinked and implemented as nested views of an "higher-order" component. For example, if we consider an app with a sidebar and a content box, we may model them as two "parallel" views or as making the sidebar the parent view and the content-pane its nested child view that depends on the selected sidebar item.
The readme itself seems to suggest the division is not really clear-cut:
Pro Tip: While multiple parallel views are a powerful feature, you'll often be able to manage your interfaces more effectively by nesting your views, and pairing those views with nested states.
When should I use multiple views and when nested views? Is there some criteria that can help you choose most of the time which is the correct way to model the states, nested vs multiple?

In my understanding, the multiple views are primarily for layout purpose, while the nested views are for parent-children hierarchical views. Using the case you mentioned as an example:
The sidebar and the content could be arranged as two distinct views:
$stateProvider.state('main', {
abstract: true,
url: '/main', //base url for the app
views: {
'': {
//this serves as a main frame for the multiple views
templateUrl: 'path/to/the/main/frame/template.html'
},
'sideBar#main': {
templateUrl: 'path/to/the/sidebar/template.html'
},
'content#main': {
templateUrl: 'path/to/the/content/template.html'
}
}
});
The path/to/the/main/frame/template.html template may contain the following frame:
<div class="row"> Header section </div>
<div class="row>
<div class="col-sm-4"><div ui-view="sideBar"></div></div>
<div class="col-sm-8"><div ui-view="content"></div></div>
</div>
Then in the sideBar or the content templates, you can nest their children subview/partials.

Nested states can be used with both nested and parallel views.
Just to note something about nested states.
What makes nested states great is that you can easily share/inherit data from parent to child view.
e.g:
Lets say you have some pages that require a user has logged in.
$stateProvider
.state('admin', {
url: '/admin'
resolve: { authenticate: authenticate }
})
.state('admin.users', {
url: '/users'
})
function authenticate(Auth) {
if (Auth.isLoggedIn()) {
// Resolve the promise successfully
return $q.when();
} else {
$timeout(function() {
// This code runs after the authentication promise has been rejected.
// Go to the log-in page
$state.go('login', {}, {reload:true});
});
// Reject the authentication promise to prevent the state from loading
return $q.reject();
}
Now every state that is a child state of admin has to pass authenticate service ( even if you navigate directly to admin/users/ ).
So basically in the resolve you can put anything and all the child states will inherit from that.
As for parallel views you have complete control over your layouts.
#TonyGW's example is great
You can use them both in your app at the same time,
I mean nested states and parallel views and you also can have parallel nested views. It really depends on the structure of your layout layout.
If you want child states to appear inside the html of the parent state you have to use nested views.

Related

With angular component and ui-router, can a single controller have multiple templates?

In a component I would like to associate templates to CRUD actions, but use a single controller that handles data for all all of them. Does this make sense with angular components, or should I use several components ?
How would the ui-router state configuration look like ?
EDIT:
Components have templates, and ui-router states too. I am confused about the articulation of those 2 concepts.
EDIT 2:
trying to clarify my understanding so far :
component are controller + template + bindings. Controller can be omitted.
states are url + template + controller or url + component. Controller can be omitted.
So it seems that components are taking away some of the responsabilities that use to belong to ui-router.
My goal here:
- url1 --> controller foo + template x;
- url2 --> controller foo + template y;
- url3 --> controller foo + template z;
should I do :
components:
component x --> controller foo + template x;
component y --> controller foo + template y;
component z --> controller foo + template z;
and then routing:
url 1 --> component x
url 2 --> component y
url 3 --> component z
?
EDIT 3:
quote from https://docs.angularjs.org/guide/component :
"In a component-based application, every view is a component"
quote from https://ui-router.github.io/guide/ng1/route-to-component :
"The component model enforces separation of concerns and encapsulation by using an isolate scope. Data from parent scopes can no longer be directly accessed. Instead, it needs to be explicitly wired in"
Yes, it is possible. Your ui-router config would look something like this: (Multiple states having same controllers.)
.state('add', {
url: '/add',
templateUrl: 'templates/add.html',
controller: 'contactCtrl'
})
.state('edit', {
url: '/edit',
templateUrl: 'templates/edit.html',
controller: 'contactCtrl'
})
Here's a working example of having multiple states and templates using same controller.
Edit: You don't have to use components. Why create three different extra components while you can achieve the same thing without them? I would still recommend the approach I mentioned above. Getting the same outcome with lesser code should always be chosen. :)
Quoting Eric Elliot on twitter,
Code is temporary. It exists while it is useful.
If code is replaced by better code, good!
If code can be deleted, celebrate!
Your state provider will look like this
JS code
$stateProvider
.state('report', {
views: {
'filters': {
templateUrl: 'report-filters.html',
controller: 'ctrl'
},
'tabledata': {
templateUrl: 'report-table.html',
controller: 'ctrl'
},
'graph': {
templateUrl: 'report-graph.html',
controller: 'ctrl'
}
}
})
in single state you can load multiple views
HTML
<body>
<div ui-view="filters"></div>
<div ui-view="tabledata"></div>
<div ui-view="graph"></div>
</body>
refer multiple views
angular.module('', [])
// Route handler touches this
.component('route1', {
template: `<shared-behaviour>
<route-view-1></route-view-1>
</shared-behaviour>`
})
// This is a purely visual component that links actions with the shared behaviour component
.component('routeView1', {
require: {
shared: '^sharedBehaviour'
},
template: '<button ng-click="$ctrl.onClick()></button>',
controller: class RouteView2Ctrl {
onClick() {
this.shared.onSharedClick()
}
}
})
// Contains the shared behaviour that is shared across the multiple routes
.component('sharedBehaviour', {
// Tell Angular to render children passed to this component
transclude: true,
template: '<div ng-transclude></div>',
controller: class SharedBehaviourCtrl {
onSharedClick() {
/// do something
}
}
})
// Newest version of UI-Router
.state('route1', {
component: 'route1'
})
Further to our comments, you could use something like the above. It's pretty verbose, but that's Angular 1.5 for you. Haven't tested that it works, but it's a basic idea of how it might work.
The main take away being that you have the route1 component which only handles linking route stuff (like the url, resolves, etc) and passes that information to its children (of which there aren't any).
routeView1 just renders how the route would work, and uses require to talk to some parent API that is shared (you could also use one-way bindings for this if you wanted, but with lots of methods this leads to a messy template).
sharedBehaviour only contains the shared behaviour you want renders any child passed to it.
This is admittedly pretty messy and I would prefer that you instead used one-way bindings for route-view-1 and handled the linking in shared-behaviour, but this might be quite a bit of repetition. You could use some transclusion magic to solve that, but it's.. not really worth it.
I would really recommend against sharing controllers like other answerers have suggested. In Angular 2 and above, a controller is the component. You share behaviour there by doing something similar to what I have done (or by using one-way bindings with callbacks) and using composition.
BTW as mentioned in my answer, newer versions of UI-Router for Angular 1.5 allow you to specify a component to be rendered rather than a template. See here for more info.

Sharing resolved data between controllers - UI Router

I'm using UI Router as a router for my application, and I have a situation where I need to have all instances of one resource available all the time.
These instances are listed in sidebar of my application.
I starter with something like this in my routes.js file:
.state('auth', {
url: '/',
abstract: true,
controller: RootCtrl,
template: '<div ui-view autoscroll="true"></div>',
resolve: {
campaigns: function(UserCampaignsCollection, activeUser) {
return (new UserCampaignsCollection).query({user_id: activeUser.id});
}
}
})
This means that I had to create separate controller, just for keeping all the campaigns and sharing those between other controllers, like this:
_app.controller('RootCtrl',
function($rootScope, campaigns) {
$rootScope.campaigns = campaigns;
});
This works fine, since I resolve it on only one place, and then all of my other states just inherit base state (as auth.account, auth.inovice etc.), but I would like to avoid need for attaching all of my campaigns on rootScope.
Is there some other way to pass data to other controllers, which would not involve creating dummy controller like this, and attaching data to rootScope?
If all other states inherit from auth then (via https://github.com/angular-ui/ui-router/wiki/Nested-States-and-Nested-Views#what-do-child-states-inherit-from-parent-states) their controllers can also be injected with resolved campaigns.

scope and controller instantiation with ui router

I am confused about when controllers get instantiated. Also, how do controllers gets instantiated when nesting states. I might be confused how scope gets attached to view and controller, that is, if every view gets its own controller and scope or do they share the same scope.
Can someone please explain when controllers get instantiated? Under nested routes do all the views share one controller and scope? What happens when I switch states and go back to a state does another controller get instantiated?
Below are my routes(config file ):
.config (googleAnalyticsCordovaProvider, $stateProvider, $urlRouterProvider, IdleProvider, KeepaliveProvider) ->
$stateProvider
.state('app', {
url: '/app',
abstract: true,
templateUrl: 'templates/menu.html',
controller: 'AppController'
})
.state('app.pincode', {
url: '/pincode',
views: {
menuContent: {
templateUrl: 'templates/pincode-yield.html',
controller: 'PincodeController'
}
}
})
.state('app.pincode.create', {
url: '/create',
views: {
pincode: {
templateUrl: 'templates/pincode-create.html',
controller: 'PincodeController'
}
}
})
.state('app.pincode.pincodeLogin', {
url: '/login',
views: {
pincode: {
templateUrl: 'templates/pincode-login.html',
controller: 'PincodeController'
}
}
})
.state('app.pincode.settings', {
url: '/settings',
views: {
pincode: {
templateUrl: 'templates/settings.html',
controller: 'PincodeController'
}
}
})
To get even more detailed answers, we can/should observe the source code and check the documentation. Let me try to explain all three questions (and also cite from code and doc).
1. When do controllers get instantiated?
Here we can observe the code of the ui-view directive:
[$ViewDirective.$inject = \['$state', '$injector', '$uiViewScroll', '$interpolate'\];][1]
Controllers are related to views. Those views, which are defined inside of a .state() as the views object:
.state('...', {
// The view definition
views : {
'' : {
template: ...
controller: ...
resolve: ..
}
},
resolve: ...
}
So, whenever is view (the ui-view) filled with settings defined inside of a state view, it acts almost as a standard, but enhanced directive.
1) Template is found,
2) Resolves are resolved
...
x) Controller is instantiated...
View targets (ui-view directives) could use names, and could be filled by different states in the hierarchy.
It could mean, that there could be a content inside of one view (e.g. title), defined by parent as well as replaced by child
// parent
.state('parent', {
views : {
'' : {...} // the main parent view, with ui-view="title"
'title#parent' : { ...} // here we go and fill parent's ui-view="title"
},
...
}
// child
.state('parent.child', {
views : {
'title' : { ...} // here we change the parent's target ui-view="title"
},
...
}
The above state definition will (whenever we transition among these two states) do:
The $state.go('parent') - the view (template, controller...) defined in 'title#parent' : { ...} will be injected into target ui-view="title" and instantiated as described above
The $state.go('parent.child') - almost the same, just the view will be taken from child state/view defintion 'title' : { ...}. That will replace the content of the ui-view="title" and will be instantiated as described above
This will be happening every time we do go from parent to child and from child to parent.
2. Under nested routes do all the views share one controller and scope?
A simple answer is NO, there is no common sharing.
In fact, each controller has its own scope, the one which is created from parent view scope. Firstly the documentation:
What Do Child States Inherit From Parent States?
...
Scope Inheritance by View Hierarchy Only
Keep in mind that scope properties only inherit down the state chain if the views of your states are nested. Inheritance of scope properties has nothing to do with the nesting of your states and everything to do with the nesting of your views (templates).
It is entirely possible that you have nested states whose templates populate ui-views at various non-nested locations within your site. In this scenario you cannot expect to access the scope variables of parent state views within the views of children states.
So, whenever is our controller (well the view with template, controller...) injected into parent's target ui-view="..." it gets inherited scope:
newScope = scope.$new();
That in a nutshell means that JS objects (e.g. scope.Model = {}) can be shared among child and parent.
$scope.Model.id = 1; // will refer to the same id in both parent & child
However, basic Javascript types are not passed by reference, and so their values are not automatically synchronised between scopes:
// set in parent
$scope.id = 1;
// in child after inherted still === 1
$scope.id = 2; // now 2 for a child, different value in parent - still === 1
It's worth reading more about prototypical inheritance here:
What are the nuances of scope prototypal / prototypical inheritance in AngularJS?
3. What happens when I switch states and go back to a state - does another controller get instantiated?
It depends.
If the parent sub view (remember ui-view="title" above) is replaced by child view, and then it is re-created (transitioning from child to parent) - such controller wil be re-initialized (discussed above).
But when we speak about the main parent view (usually unnamed), which represents the parent (For example the unnamed view below with controller 'ParentMainCtrl')
.state('parent', {
views : {
'' : { // // the main parent view
controller: 'ParentMainCtrl',
}
'title#parent'
'tooltip#parent'
},
Then we can be sure that such controller is NOT re-instantiated. It lives during the lifetime of all its children, plus a parent's one (no child state selected).
To re-load this view/controller, we have to use an option reload
$state.go(to, params, options)
...
options Options object. The options are:
...
reload - {boolean=false}, If true will force transition even if the state or params have not changed, aka a reload of the same state. It differs from reloadOnSearch because you'd use this when you want to force a reload when everything is the same, including search params.
Hope that helps a bit. For further information, check out these resources:
Nested States & Nested Views
Multiple Named Views
API Reference
State.js of the sample application - I would say the best documented piece of code ever
Controllers get instantiated whenever you visit the specific state. For example, while visiting app.pincode.pincodeLogin for the first time one AppController and two PincodeControllers are constructed, each with its own view assuming you got the templates right. Switching to 'app.pincode.settings' would destroy the innermost controller and replace it with a new one, althought the two controllers higher at the hierarchy will not be touched. Scopes follow the standard AngularJS' inheritance pattern, they are not isolated.
You probably would want to remove the controllers in the sub states (and handle the business logic in the parent controller) or have a distinct controller for each state - the same controller for different templates and views is usually a sign of bad design.
Controllers get instantiated when the corresponding views are loaded for the first time.
For example if you have 3 tabs associated with 3 controllers - then the controller associated with the default view instantiates First. Next when you load the other views the associated controllers also get instantiated.
But interestingly, once a view is loaded in the DOM - it is cached by-default. When a view is navigated away from, its element is left in the DOM, and its scope is disconnected from the $watch cycle. When navigating to a view that is already cached, its scope is then reconnected, and the existing element that was left in the DOM becomes the active view.

Nested Views VS Separate

I'm trying to decide on an Angular Structure for my app. I started with the Angular Full Stack Generator.
In UI-Router, you can nest states. It seems logical for me to nest related states. ex: Registration, Registration.form, Registration.confirmation.... but those nested states seem to rely on each child view and parent view being viewed on the same page.
Meaning the parent will have an <ui-view> tag that will insert the child view when called.
Is it possible to have a nested state replace the parent?
Or do I have just make unnested states?
Or is this a case for an abstract state?
There is a working example
Child replacing parent is almost "native" UI-Router solution. Let's have this state definitions:
.state('parent', {
url: "/parent",
templateUrl: 'tpl.html',
})
.state('parent.child', {
url: "/child",
templateUrl: 'tpl.html',
controller: 'ChildCtrl',
})
And this would be the tpl.html, in our case used for both states:
<div ui-view>
<h3>current state name: <var>{{$state.current.name}}</var></h3>
...
</div>
So, what we can see, parent root HTML element, does have attribute ui-view. That means, that this element also plays a role of a target for a child.
Check it in action here

AngularJS UI-Router, A state view for all url expect one?

Is it possible with ui-router (plugin for AngularJS), to have a state (and his template) for all url expect one or two ?
Thx guys !
If there are things you show in the normal, user-accessible area of your app, but not in the "admin" section, I would organize it such that you have two "base" views, like so:
$stateProvider.state("app", {
abstract: true,
views: {
"graph#": { /* view config */ }
}
}).state("admin", {
abstract: true
});
Then, all of your other states will inherit from one or the other (i.e. "app.foo" or "admin.foo"). As long as you have a <ui-view name="graph"></ui-view> in the root of your base template, it will populate with content in your "app" states, but not your "admin" states. Hope that makes sense.

Resources