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)
Related
As displayed below, even though i've 2 diff html i am using same controller so i want to have the scope shared between 2 html.
Is it possible to configure this in routeProvider?
.when('/cform', {
templateUrl: '/partials/clientForm.html',
controller: 'ClientCtrl', //Same Controller
})
.when('/vwClientManagement', {
templateUrl: '/partials/clientManagement.html',
controller: 'ClientCtrl' //Same Controller
})
$scope.showClientAddUpdateView = function(action,clientId) {
$scope.client.name="Hello World"; // This is getting displayed in UI.
$http.get("/qCli", { params:{id : clientId}}).then(
function successCallback(response){
//$scope.client = response.data;
$scope.client.name="Hello World After Ajax"; // This is not getting displayed in UI.
$location.path('/cform');
},function errorCallback(response){
console.log("err searching clients");
}
);
};
Update:
Scanario 1 - changing route and setting scope in ajax success call back, loosing only those value which i have set in success callback after route change
Scanario 2 - updating scope in ajax success callback but not changing route, view is updated with correct values.
Added More details More Details
In my opinion, it's better to use Service, for sharing data between Angular controllers. Generally, angularjs is very flexible, and there is at least few ways to solve your problem: using services, using $state.go services, using stateparams, using rootscope...
Check this issue, there is a bunch of related and useful advices: Share data between AngularJS controllers
While using this:
.when('/cform', {
templateUrl: '/partials/clientForm.html',
controller: 'ClientCtrl', //Same Controller
}).when('/vwClientManagement', {
templateUrl: '/partials/clientManagement.html',
controller: 'ClientCtrl' //Same Controller
})
You are not using one unique controller. You are creating, for each view, an instance of ClientCtrl.
So they don't share there scope, each have a different and independant scope.
It seems you are trying to avoid using services whereas this would be best done in this case. There are severals (1, 2) answers on Stack Overflow dealing with this subject.
I have the following usecase:
I have two views which are identical, as well as two controllers which are very similar. I intend to extend one controller with the other and just override the diff. The problem I am having is in ui-router I want to choose a controller by name (Since the templates is shared, and the controllers differ this cannot be declared in the template itself)
Before this refactoring I had the following code:
.state('edit-page-menu', {
url: '/sites/:siteId/page_menus/:menuId',
templateUrl: 'partials/edit-menu.html',
controller: ['$scope', '$stateParams', (scope, stateParams) => {
scope.siteId = stateParams.siteId
scope.menuId = stateParams.menuId
}]
})
Which adds the stateParams to the scope. In this usecase I had the controller defined in the template and could modify the scope that way. However now when I switch to controller by name approach
.state('edit-page-menu', {
url: '/sites/:siteId/menus/:menuId',
templateUrl: 'partials/edit-menu.html',
controller: 'editMenuCtrl'
})
I don't know how to inject / add the $stateParams to the controller. And I would really like to avoid injecting "$state" in the controller and fetch the parameters that way. Is there a way to modify the scope of a controller if you choose it by name?
Best regards.
I've got a form with hundred of fields.
As part of a secondary controller I'm trying to avoid the $scope to bind the data to my view. The way I go is:
angular
.module('app')
.controller('SequencesController', SequencesController) // main controller for the view
.controller('AccordionCtrl', AccordionCtrl); // the one I'm targetting
AccordionCtrl.$inject = ['$scope','$http'];
function AccordionCtrl($scope,$http) {
this.foo = "bar";
}
I'm using this on my view but it's not working.
<div ng-controller="AccordionCtrl as acc">
Acc : {{ acc.foo }}
</div>
Using $scope does work though. Any ideas?
Extra information:
The view is binded to a main controller that's called like that in app.js
.when('/sequences', {
controller: 'SequencesController',
templateUrl: 'app/Sequences/sequences.view.html',
controllerAs: 'vm'
})
When you are trying to access to the object(controller) property: {{ acc.foo }}, which angular is doing behind is: $scope.acc.foo, because this is how angular works, you must define your variables inside the scope.
If you dont want to interfer in the showing variables, create two diferent objects inside the scope: $scope.viewVars and $scope.backVars and add them all properties you need separately.
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.
I'm using ngRoute, with the following configuration :
myApp.config(['$routeProvider', function($routeProvider) {
$routeProvider
.when('/view1', {
controller:'my1Ctrl',
templateUrl:'views/view1.html'
})
.when('/view2', {
controller:'my2Ctrl',
templateUrl:'views/view2.html'
});
}])
For the purpose of this example, I'm logging the ID of the controller every second. So, in the my1Ctrl controller, I add the following code :
$scope.f = function(){
console.log('Test Controller, id: ', $scope.$id);
setTimeout($scope.f, 1000);
};
setTimeout($scope.f, 1000);
Finally, I launch my app and navigate 3 times from view1 to view2. After that, every second, I get the following log:
Test Controller, id: 00A
Test Controller, id: 00I
Test Controller, id: 00E
If my test case is not relevant, please tell me.
My question: When ngRoute instanciates a new controller, is the old one removed or is it still in memory? If it is, can I configure ngRoute to re-use the same controller or to distroy the old one ?
$scope.$id is not the id of the controller but of the scope instead.
When you open new page defined with $routeProvider AngularJS "instantiates" new controller (i.e. calls the internal method instantiate). This method invokes the controller's function with new so it creates new object. But it's not all AngularJS does. Before calling the controller's constructor with new, AngularJS resolves all it's dependencies. If the controller has local dependency called $scope AngularJS creates new $scope, each scope has unique identifier - $scope.$id.
Of course, in order to prevent memory leaks AngularJS does not keep old controllers in the memory, so you shouldn't worry about this.