I'm having a hard time trying to understand the best way to achieve inheritance with my controllers. I've seen a few other posts here about these but I still donĀ“t get some things.
Here's what I have:
- 2 controllers which are 80% similar. I already have a factory which both use to get the data which will be displayed.
- I use the controllerAs notation, with var vm = this
- there's a mix of vars and functions which will be used in the view and therefore are created inside vm, and some other internal vars and functions which are not.
- so I tried to create a single parent controller with all this and then use injection to create these 2 controllers, overwriting only what I need, but this is not working as I expected and also I'm not sure this is the right thing to do
Here is a simplified version of the code.
(function() {
angular
.controller('ParentController', ParentController)
ParentController.$inject = [ '$scope', '$location' ];
function ParentController($scope, $location) {
var vm = this; // view model
var URL = $location.url();
var isDataLoaded = false;
vm.predicate = 'order';
vm.reverse = false;
vm.results;
vm.isDataReady = isDataReady;
vm.setOrder = setOrder;
function isDataReady() {
return isDataLoaded;
}
function setOrder(p) {
vm.reverse = (p === vm.predicate) ? !vm.reverse : false;
vm.predicate = p;
}
$scope.$on('READ.FINISHED', function() {
isDataLoaded = true;
})
}
})();
-
(function() {
angular
.controller('ChildController', ChildController)
ChildController.$inject = ['$controller', '$scope', 'myFactory'];
function ChildController($controller, $scope, myFactory) {
$controller('ParentController', {$scope: $scope});
var TEMPLATE = 'SCREEN';
// ************** M A I N **************
myFactory.getResults(URL, vm);
}
})();
This is now working as I expected.
When I inject ChildController with ParentController, do I really need to inject the $scope? I'm actually using vm. Also, do I need to inject also $location? In this example when I execute my code I'm forced to use var URL = $location.url(); again in my ChildController, I expected to inherite the value from ParentController.
So the thing is, am I only getting values from $scope if I work like this? what about vm? and what about those vars/functions declared outside vm like var isDataLoaded?
I'd appreciate some insight about this. Would this be the right way to do it?
Many thanks.
EDIT: Ok, I found out how to use my ControllerAs syntax with this. Code in the child controller would be like this:
function ChildController($controller, $scope, myFactory) {
$controller('ParentController as vm', {$scope: $scope});
var vm = $scope.vm;
var TEMPLATE = 'SCREEN';
// ************** M A I N **************
myFactory.getResults(URL, vm);
}
But I still need to get a way to also recover the regular var/functions inside the parent controller. Any ideas? Can it be done cleanly?
I recommend you to implement usual javascript's inheritance mechanism between two classes. At ChildController constructor you will execute ParentController constructor and pass to it injected parameters (without using $controller service).
I created simple example (very far from your logic but consider it as patten). I have two controllers: ParentController and ChildController, that inherited from first one. I used only ChildController with $scope, $interval and custom service with name "myservice" (all of them needed only for example). You can see that I used methods and fields from parent and child controllers. Logic of my app is very simple: you can add new items to collection (by means of ParentController) and remove them(by means of ChildController) with logging.
At that case I recommend you use ChildController as ctrl for Data Binding instead of $scope, because it more in line inheritance paradigm (we inherite controllers(ctrl) not $scope).
P.S. If you be going to use inheritance very often I recommend you to use TypeScript - it gives very simple and flexible solution of this problem in c# style.
Controllers.js
function ParentController(myservice, $scope, $interval) {
this.newOne={};
this.lastacivity={};
this.$interval = $interval;
this.items = myservice();
}
ParentController.prototype.Log = function(item){
var self = this;
this.$interval(function(){
console.log(item);
self.lastacivity = item;
}, 100, 1);
}
ParentController.prototype.AddNew = function (Name, Age) {
var newitem = {
name: this.newOne.Name,
age: this.newOne.Age
}
this.items.push(newitem);
this.Log(newitem);
}
function ChildController(myservice, $scope, $interval) {
//Transfering myservice, $scope, $interval from ChildController to ParentController constructor,
//also you can pass all what you want: $http, $location etc.
ParentController.apply(this, [myservice, $scope, $interval]);
}
ChildController.prototype = Object.create(ParentController.prototype)
//your ChildController's own methods
ChildController.prototype.Remove = function (item) {
this.items.splice(this.items.indexOf(item), 1);
this.Log(item);
}
script.js
(function(angular) {
'use strict';
angular.module('scopeExample', []).factory('myservice', function() {
var items = [
{name:"Mike",age:21},
{name:"Kate",age:22},
{name:"Tom",age:11}
];
return function(){
return items;
}
}).controller('ChildController', ['myservice', '$scope', '$interval', ChildController]);
})(window.angular);
HTML
<body ng-app="scopeExample">
<div ng-controller="ChildController as ctrl">
<label>Name</label>
<input type='text' ng-model='ctrl.newOne.Name'/>
<label>Age</label>
<input type='text' ng-model='ctrl.newOne.Age'/>
<input type='button' value='add' ng-click='ctrl.AddNew()' ng-disabled="!(ctrl.newOne.Name && ctrl.newOne.Age)"/>
<br>
<br>
<table>
<thead>
<tr>
<th>Name</th>
<th>Age</th>
<th></th>
</tr>
</thead>
<tbody>
<tr ng-repeat="item in ctrl.items">
<td>{{item.name}}</td>
<td>{{item.age}}</td>
<td>
<input type='button' value='X' ng-click='ctrl.Remove(item)'/>
</td>
</tr>
</tbody>
</table>
<p>{{ctrl.lastacivity | json}}</p>
</div>
</body>
Related
I've been having a lot of trouble with understanding scope inheritance and I've tried my best to pass data objects from a parent controller into the child controller, but I can't seem to get things to work. Can someone explain why this isn't functioning? Thank you!
EDIT: I didn't specify this earlier, but it's a project requirement to use the John Papa style guide, so I can't solve this problem by using $scope in either of the controllers.
UPDATE: It seems I misunderstood the purpose of using this... based on help from posters below, I now understand that certain actions require the use of $scope and John Papa's style guide simply asks developers to use this when appropriate to avoid scope conflicts, not as a replacement for scope
JS
//parent.controller.js
(function () {
'use strict';
angular
.module('app')
.controller('ParentController', ParentController);
ParentController.$inject = ['$scope'];
function ParentController($scope) {
var vm = this;
console.log(this);
vm.test = {};
vm.test.label = "This is being set in the parent controller.";
}
})();
//child.controller.js
(function () {
'use strict';
angular
.module('app')
.controller('ChildController', ChildController);
ChildController.$inject = ['$scope'];
function ChildController($scope) {
var vm = this;
vm.test = vm.test;
}
})();
HTML
<div ng-controller="ParentController as vm">
<div>PARENT: {{vm.test.label}}</div>
<div ng-controller="ChildController as vm">
<div>CHILD: {{vm.test.label}}</div>
</div>
</div>
RESULT
PARENT: 'This is being set in the parent controller.'
CHILD:
Issue is: vm is also the part of the $scope itself. So you can not have same this instance for Parent & Child controller. Otherwise you will be facing issues while using them.
If you want to isolate this instance for Parent & Child then give different names.
Since vm is also the part of the controller so if you want to access Parent's vm inside Child controller then you will have to do $scope.vm
Working code as per your requirement is attached below:
Controller
---------
(function () {
'use strict';
angular
.module('app', [])
.controller('ParentController', ParentController);
ParentController.$inject = ['$scope'];
function ParentController($scope) {
var vm = this;
console.log(this);
vm.test = {};
vm.test.label = "This is being set in the parent controller.";
}
})();
(function () {
'use strict';
angular
.module('app')
.controller('ChildController', ChildController);
ChildController.$inject = ['$scope'];
function ChildController($scope) {
var childVm = this;
childVm.test = $scope.vm.test;
}
})();
HTML
---
<div ng-app="app">
<div ng-controller="ParentController as vm">
<div>PARENT: {{vm.test.label}}</div>
<div ng-controller="ChildController as childVm">
<div>CHILD: {{childVm.test.label}}</div>
</div>
</div>
</div>
Cheers!
After checking the plnkr,hope I was able to understand your question, and giving answer based on it:
In controller file, for ChildController
var childCtrl = this;
// Why don't either of these work?
// childCtrl.test = parentCtrl.test;
// childCtrl.test = this.parentCtrl.test;
assigning parentCtrl.test, doesn't makes any sense, as it is the object of the parent controller.
this.parentCtrl.test will get evaluated to childCtrl.parentCtrl.test which is invalid.
Following worked because-
// But this does...
childCtrl.test = $scope.parentCtrl.test;
During the code execution
Two separate scopes will get created each for ParentController and ChildController.
child will inherit the properties of parent, those which are not present in childController and will be assigned to the childController scope.
Due to which you where able to access value in child with scope.
If i'd like to use the "Controller as ..." syntax in Angular, how should I approach things like $scope.$on(...) that i need to put inside the controller?
I get an impression i could do it some other way than the one shown below.
Here, to get $scope.$on working i bind "this" to the callback function. I tried to invoke $on on "this" inside the controller but it didn't work.
Could you give me a hint here or if i'm completely messing up, could you point me to some right way to do it? Thanks.
main.js:
angular.module('ramaApp')
.controller('MainCtrl', ['$scope', '$location', function ($scope, $location) {
this.whereAmINow = 'INDEX';
$scope.$on('$locationChangeStart', function(event) {
this.whereAmINow = $location.path();
}.bind(this));
this.jumpTo = function(where) { $location.path(where); }
}]);
index.html:
<div ng-controller="MainCtrl as main">
<p>I am seeing the slide named: {{ main.whereAmINow }}</p>
<div ng-click="main.jumpTo('/slide1')">Slide 1</div>
<div ng-click="main.jumpTo('/slide2')">Slide 2</div>
<div ng-click="main.jumpTo('/slide3')">Slide 3</div>
</div>
As far as I know, you need to inject $scope if you want $scope watchers/methods. ControllerAs is just syntactic sugar to enable to see more clearly the structure of your nested controllers.
Three ideas though which may simplify your code.
Use var vm = this, in order to get rid of the bind(this).
var vm = this;
vm.whereAmINow = "/";
$scope.$on('$locationChangeStart', function(event) {
vm.whereAmINow = $location.path();
});
vm.jumpTo = function(where) {
$location.path(where);
}
The whole whereamINow variable makes sense putting it into the initialization of app aka .run() (before config) since I assume it's a global variable and you don't need to use a $scope watcher/method for it.
Another option is to use a factory to make the changes persist, so you simply create a location factory which holds the current active path.
Inject $scope and your controller is accessible by whatever you named it
EG:
$stateProvider
.state('my-state', {
...
controller: 'MyCtrl',
controllerAs: 'ctrl',
...
});
.controller('MyCtrl', function($scope) {
var $this = this;
$scope.$on('ctrl.data', function(new, old) {
// whatevs
});
$timeout(function() {
$this.data = 'changed';
}, 1000);
});
Ok, i think people just do the same, just as in this question:
Replace $scope with "'controller' as" syntax
The Sample Code For The Question Is Here!
I have some calculated data for some fields. And I get the data using the "total" approach in my example. I understand that the function will be called when the controller is initialized once and then again to get the value itself. But what I realized is the "total" function is being called twice for each when I execute the compile function with the button click. Where total has nothing related with the compile or the data which is used in compile. What is the reason these useless(for my case:) ) calls?
// JS CODE
var myApp = angular.module('myApp',[]);
function MyCtrl($scope) {
$scope.val1 = 1;
$scope.val2 = 2;
$scope.serverData = 99;
$scope.total = function() {
console.log('I am total');
return $scope.val1 + $scope.val2;
}
$scope.compileData = function(){
var injector = angular.element($('#serverDataHere')).injector(),
compile =injector.get('$compile');
$('#serverDataHere').append(compile('<div>{{serverData}}</div>')($scope));
};
}
//// HTML CODE
<div ng-controller="MyCtrl">
<table border="1">
<tbody>
<tr>
<td>{{val1}}</td>
<td>{{val2}}</td>
<td>{{total()}}</td>
<td>{{total()}}</td>
<td>{{total()}}</td>
</tr>
<tr>
<td>Computer</td>
<td>Computer</td>
<td>Computer</td>
<td id="serverDataHere"></td>
<td>X Games</td>
</tr>
</tbody>
</table>
<button ng-click="compileData()">Compile Data</button>
</div>
You are kicking off a digest cycle, which runs ALL evaluations on your page. The {{ }}.
On a side note, you should not do DOM manipulation in a controller, that is why directives exist.
There is also never a reason to access the $injector, $compile, or any service directly from the module. You should inject them using the standard injection of Angular.
myApp.controler('controllerName', function($scope, $compile) {
return ctrlFunctions;
});
(Not sure if I understand your question correctly.)
But you can just run your total() function once in the controller, assign it to a scope variable and then bind it to the view.
var totalFcn = function() {
console.log('I am total');
return $scope.val1 + $scope.val2;
}
$scope.total = totalFcn();
Then in your view, just put the total variable instead of the function call.
<td>{{total}}</td>
With the original way to define controllers, accessing the parent's scope was fairly trivial, since the child scope prototypically inherits from its parent.
app.controller("parentCtrl", function($scope){
$scope.name = "Parent";
})
.controller("childCtrl", function($scope){
$scope.childName = "child of " + $scope.name;
});
<div ng-controller="parentCtrl">
{{name}}
<div ng-controller="childCtrl">
{{childName}}
</div>
</div>
The Controller-As approach seems to be the recommended way to declare a controller. But with Controller-As, the above approach no longer works.
Sure, I can access the parent scope with pc.name from the View:
<div ng-controller="parentCtrl as pc">
{{pc.name}}
<div ng-controller="childCtrl as cc">
{{cc.childName}}
</div>
</div>
I do have some issues with this (potential for spaghetti code), but this question is about accessing the parent scope from the child controller.
The only way I can see this working is:
app.controller("parentCtrl", function(){
this.name = "parent";
})
.controller("childCtrl", function($scope){
$scope.pc.name = "child of " + $scope.name;
// or
$scope.$parent.pc.name = "child of " + $scope.name;
// there's no $scope.name
// and no $scope.$parent.name
});
So now, the child controller needs to know about "pc" - except, this should (in my mind) be restricted to the view. I don't think a child controller should know about the fact that a view decided to declare a ng-controller="parentCtrl as pc".
Q: What's the right approach then?
EDIT:
Clarification: I'm not looking to inherit a parent controller. I am looking to inherit/change the shared scope. So, if I was to amend the first example, I should be able to do the following:
app.controller("parentCtrl", function($scope){
$scope.someObj = {prop: "not set"};
})
.controller("childCtrl", function($scope){
$scope.someObj.prop = "changed";
});
After researching, I came to the following realization:
Controller-As approach is NOT a substitute for using $scope. Both have their place, and can/should be used together judiciously.
$scope does exactly what the name implies: i.e. it defines ViewModel properties on the $scope. This works best for sharing scope with nested controllers that can use the $scope to drive their own logic or to change it.
Controler-As defines the entire controller object as a ViewModel with a named scope (via the controller's alias). This works best only in the View (but not other controllers), if the View decides if it wants to reference a specific controller ViewModel.
Here's an example:
var app = angular.module('myApp', []);
// Then the controllers could choose whether they want to modify the inherited scope or not:
app.controller("ParentCtrl", function($scope) {
this.prop1 = {
v: "prop1 from ParentCtrl"
};
$scope.prop1 = {
v: "defined on the scope by ParentCtrl"
};
})
.controller("Child1Ctrl", function($scope) {})
.controller("Child2Ctrl", function($scope) {
// here, I don't know about the "pc" alias
this.myProp = $scope.prop1.v + ", and changed by Child2Ctrl";
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.4.8/angular.min.js"></script>
<body ng-app="myApp">
<div ng-controller="ParentCtrl as pc">
<div ng-controller="Child1Ctrl">
<div>I know about the "pc" alias: {{pc.prop1.v}}</div>
</div>
<div ng-controller="Child2Ctrl as ch2">
<div>I only care about my own ViewModel: {{ch2.myProp}}</div>
</div>
</div>
You should do like :
html
<div ng-controller="ChildController as child">
<button type="button" ng-click="child.sayMe()">Say me!</button>
</div>
js
var app = angular.module('myApp', [])
app.controller('BaseController',function() {
this.me = 'Base';
this.sayMe= function() {
alert(this.me);
}
});
app.controller('ChildController', function($scope, $controller) {
var controller = $controller('BaseController as base', {$scope: $scope});
angular.extend(this, controller);
this.me = 'Child';
});
take a look at https://docs.angularjs.org/guide/controller
For anyone looking to simply access the parent scope programmatically, use the $scope service, find the parent scope, and access the name used for the parent scope's controllerAs, e.g.:
$scope.$parent.someName.doSomething();
I recently started using AngularJS and the way I'm building my apps now is like this:
MainController.js
var app = angular.module('app', ['SomeController', 'MainController']);
app.controller('MainController', function ($scope) {
// do some stuff
}
SomeController.js
var SomeController= angular.module('SomeController', []);
SomeController.controller('SomeController', function ($scope) {
$scope.variable = "test";
// do some otherstuff
}
The problem that Im' running into is that the scope is not being shared between modules. From MainController I can't get the variable "test" for example.
What is the best practice for this? Do I store all my controllers in 1 module in 1 file?
How can i have 1 page with 2 controllers and share the $scope between them, or is it OK to put everything in just one controller ?
You could use a service like this: Live demo here (click).
JavaScript:
var otherApp = angular.module('otherApp', []);
otherApp.factory('myService', function() {
var myService = {
someData: ''
};
return myService;
});
otherApp.controller('otherCtrl', function($scope, myService) {
$scope.shared = myService;
});
var app = angular.module('myApp', ['otherApp']);
app.controller('myCtrl', function($scope, myService) {
$scope.shared = myService;
});
Markup:
<div ng-controller="otherCtrl">
<input ng-model="shared.someData" placeholder="Type here..">
</div>
<div ng-controller="myCtrl">
{{shared.someData}}
</div>
Here's a nice article on sharing data with services.
You can also nest controllers to have the parent controller's scope properties inherited by the child scope: http://jsbin.com/AgAYIVE/3/edit
<div ng-controller="ctrl1">
<span>ctrl1:</span>
<input ng-model="foo" placeholder="Type here..">
<div ng-controller="ctrl2">
<span>ctrl2:</span>
{{foo}}
</div>
</div>
But, the child won't update the parent - only the parent's properties update the child.
You would use "the dot rule" to have updates on the child affect the parent. That means nesting your properties in an object. Since the parent and child both have the same object, changes on that object will be reflected in both places. That's just how object references work. A lot of people consider it best practice to not use inheritance, but put everything in directives with isolated scope.
You can use $rootScope, each Angular application has exactly one root scope.
Reference
app.controller('MainController', function ($scope, $rootScope) {
$rootScope.data = 'App scope data';
}