scope and controller instantiation with ui router - angularjs

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.

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.

Angular ui-router: nested views vs multiple views

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.

ui-router child state's view not showing

I'm developing an application where I have the use for a child state. The purpose of making a child state is
To inherit the resolve
To inherit the url base
When I navigate to the child state, the parent state's view and controller is initialized. The child controller isn't initialized at all, and the view isn't showing at all. One thing that I think is weird is that the child view is actually loaded over XHR, but then it never seems to appear.
How can I make the child state's view appear, and the child state's controller initialize with resolves from the parent state?
.state('restaurant', {
url: "/{city:[a-zA-ZåäöÅÖÄ]{2,}}/{restaurantUrl:[a-zA_ZåäöÅÄÖ\-]{2,}}",
views: {
"main": {
templateUrl: "/views/restaurant.html",
controller: "RestaurantCtrl",
resolve: {
restaurant: function($q, $stateParams, RestaurantsSrvc) {
/*Some stuff going on that returns a promise*/
}
}
}
}
})
.state('restaurant.checkout', {
url: "/checkout",
views: {
"main": {
templateUrl: "/views/checkout.html",
controller: "CheckoutCtrl"
}
}
})
Add the <div ui-view="main"></div> to restaurant.html. Populating ui-view elements outside of the parent template is apparently not possible.
Also make sure that there is one ui-view per template with child states. When you have only one place to insert child templates, don't use named views, they are for cases where multiple child views need to be shown at the same time. The sketch in the documentation illustrates this use case nicely.
Also note that by default the parent view is shown when a child state is active, because the ui-view for the child is within the parent template. If you need to hide the parent stuff (or just parts of it) use ng-hide with the $state service as indicated by this answer.

Controller from Parent Layout is not access by child views

I have a layout set up for one of my pages that is then seeded with a ton of little views that I use to populate with date. My states currently looks like so:
.state('eventLayout', {
templateUrl: 'partials/layouts/event.html',
controller: 'EventCtrl',
})
.state('event', {
parent: 'eventLayout',
url: '/event/{eventUrl}',
views: {
'event.video': {
templateUrl: 'partials/views/event.video.html'
},
'event.info': {
templateUrl: 'partials/views/event.info.html'
},
'card.event': {
templateUrl: 'partials/views/card.event.html'
},
'card.clip': {
templateUrl: 'partials/views/card.clip.html'
},
'card.upcoming': {
templateUrl: 'partials/views/card.upcoming.html'
},
'card.banner': {
templateUrl: 'partials/views/card.banner.html'
},
'card.comment': {
templateUrl: 'partials/views/card.comment.html'
},
'card.notification': {
templateUrl: 'partials/views/card.notification.html'
},
'card.cube': {
templateUrl: 'partials/views/card.cube.html'
},
'card.mix': {
templateUrl: 'partials/views/card.mix.html'
},
'card.score': {
templateUrl: 'partials/views/card.score.html'
},
'card.sponsor': {
templateUrl: 'partials/views/card.sponsor.html'
},
'card.nobroadcasters': {
templateUrl: 'partials/views/card.nobroadcasters.html'
},
'card.link': {
templateUrl: 'partials/views/card.link.html'
},
'card.suggest': {
templateUrl: 'partials/views/card.suggest.html',
controller: 'SuggestblockCtrl'
},
'card.footer': {
templateUrl: 'partials/views/card.footer.html'
}
}
})
As you can see the parent layout holds my Controller for the page which is called EventCtrl . Now I would expect that all the views now have access to this controller, but that doesn't seem to be the case. Instead I have to wrap the main parent template from eventLayout in a div where I then just use the old school:
<div ng-controller="EventCtrl"></div>
I'd like to at least understand why this is happeneing and what the proper method is to make sure all views have access to the states main controller. Thanks!
EDIT:
To add more context to how im using the views in my current app I have detailed the current set-up below.
From the file partials/layouts/event.html in parent $state eventLayout
<div ng-controller="EventCtrl">
<div ui-view="event.video"></div>
<div ng-repeat="activity in activities.results">
<div ng-if="activity.card_type == 'event'" ui-view="card.event"></div>
<div ng-if="activity.card_type == 'clip'" ui-view="card.clip"></div>
<div ng-if="activity.card_type == 'upcoming'" ui-view="card.upcoming"></div>
</div>
</div>
</div>
As you can see a views are nested within the parent layout. I'm having to wrap it all with ng-controller="EventCtrl" in order to allow each view access to its scope.
The overall angular's ui-router design, is about the view / $scope inheritance - not base controller accesibility. The detailed info could be found here:
Scope Inheritance by View Hierarchy Only
small cite:
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...
Also these are good reading, which content would be hardly any better explained here:
AngularJS Inheritance Patterns by Minko Gechev
AngularJS–Part 3, Inheritance by Gabriel Schenker
So, let's summarize a bit.
1) We know, that from any template we can access only its own $scope.
2) What is available in the view/template $scope, is a job of its Controller which can extend it with some functions, objects, etc.
3) If any parent controller (from view-nesting perspective) will inject anything into its own/parent scope - we will have access to it as well (so called prototypical inheritance)
Having this, we can create an Events object in the parent $scope, managed by EventCtrl - and consume it in any a child view template:
// the reference (reference to object Events)
// to be shared accross all child $scopes
$scope.Events = {};
// objects
$scope.Events.MyModel = { FirstName: "....
// functions
$scope.Events.save = function () {....
And now in any child template we can use it like this
<div>{{Events.MyModel.FirstName}}</div>
Another technique would be to place the controller into $scope's Events object:
$scope.Events = this; // controller
And then have full access to controller's methods, properties...

two-way binding in angular not updating

I have created a plnkr which demonstrates a problem that I am trying to solve. When you click the link in the plnkr, you will see a textfield. This textfield is bound with ng-model to myCtrl.foo, and in that controller is a $watch looking at the controller's foo property and then setting $scope.num to a random number. You will notice the random number never changes even though the watcher is clearly firing (via a console.log).
http://plnkr.co/edit/wpFPFeRC6CFFjLOa9QQw
Can anyone explain why this is not working, and what I can do to fix it?
Here is what happens
When you define your routes:
app.config(function ($stateProvider) { $stateProvider
.state('items', {
url: '/items/:item_id',
views: {
'my-view': {
controller: 'myController as myCtrl',
templateUrl: 'my-view.html'
},
'main#': {
controller: 'myController',
templateUrl: 'main.html'
},
}
})
});
you assign 2 different views to use the same controller, which is OK, but a controller in Angular is not a singleton. It is a constructor function. Meaning that both controllers (and their scope) will not be the same instance, but 2 different instances.
So the controller and the scope in view 1 will not be the same controller and scope as in view 2.
The controller will be instantiated twice with a different scope so the changes made in the scope of view 1 will not reflect the changes made in the scope of view 2 (as they have a different scope).
You can see this if you add the following lines to your controller:
app.controller('myController', function($scope) {
console.log('myController scope id: ' + $scope.$id);
console.dir($scope);
// Your code here
});
The log will show:
myController scope id: 003
myController scope id: 004
Possible solutions
Avoiding this boils down to personal preference. Here are some valid options:
use events to communicate between scopes and send an event when num is updated
use a service to store num centrally
store num in the $rootScope
Hope that helps!
Here is it working: http://plnkr.co/edit/tagldRNsgLXUhoGfZ2Un?p=preview
Did two things, first assigned all primitive bindings to an object called test. You can see why it is best to do this here: https://egghead.io/lessons/angularjs-the-dot
Second, put the controller around the views to ensure they share a scope and pulled it out of ui-router (as I know next to nothing about ui-router and whatever magic it does)

Resources