AngularJS Controller Inheritance: "this" not pointing to the child controller - javascript

I am using the controllerAs syntax.
I used $controller to inherit from a parent (more like a base or abstract) controller. I found this question not long ago which I based on.
I noticed that when I use a function which uses a controller property (this.propName), it does not use the current controller this, but the parent's. Here's a demo (plunkr).
Here's a gist to both my parent controller and child controller.

Update sayMyName method to following:
function sayMyName() {
alert(this.me);
}
As you are trying to pick me property on the base controller the alert should pick me value from the corresponding instance and not the instance when it was created which is vm
Updated plunker link

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';
});

Related

Angular controller inheritance scoping issues

The goal here is to have two different directives that are technically siblings share functionality. I will either use one or the other, never one inside the other.
However, the second directive will have all the capability of the first with some small additions. Because of this, I would like the functionality to inherit from the "Parent" directive to the "Child".
I'm achieving this by re-using the same directive definition object from the Parent on the Child, with the exception of the controller/template fields being changed.
This was all working well up until I hit the watchers from my ParentDirCtrl. For some reason the watcher seems to be set up correctly watching mydir.obj1 and yet somehow inside the watcher callback function mydir.obj1 becomes undefined.
I'm assuming something about _.extend/$controller is changing how the $scopes work so mydir.obj1 isn't defined in the ParentDirCtrl, but I'm not sure why that would be the case.
Plunk
angular.module('plunker', [])
// lodash
.constant('_', _)
.controller('MainCtrl', function($scope, $timeout) {
$scope.obj = {
name: 'John',
age: 30,
};
})
.controller('ParentDirCtrl', function($scope) {
var mydir = this;
mydir.doStuffInParent = function() {
alert('executed from the parent directive');
}
$scope.$watch('mydir.obj1', function() {
// ====================================
// ERROR
// Why is 'mydir.obj1' undefined when
// occupation is set?
// ====================================
mydir.obj1.occupation = 'Meteorologist';
});
})
.directive('parentDirective', parentDirective)
.directive('childDirective', function() {
// borrow the directive definition object from the parent directive
var parentDDO = parentDirective();
// uodate the template and controller for our new directive
parentDDO.template = [
'<div>',
'<p ng-click="mydir.doStuffInParent()">{{mydir.obj1.name}}</p>',
'<p ng-click="mydir.doStuffInChild()">{{mydir.obj1.age}}</p>',
'</div>'
].join('');
parentDDO.controller = function($scope, $controller, _) {
// extend 'this' with the Parent's controller
var mydir = _.extend(this, $controller('ParentDirCtrl', { $scope: $scope }));
mydir.doStuffInChild = function() {
alert("executed from the child directive");
};
};
return parentDDO;
});
// this will be moved to the top during declaration hoisting
function parentDirective() {
return {
restrict:'E',
scope: {},
bindToController: {
obj1: '=',
},
template: '<div>{{mydir.obj1}}</div>',
controller: 'ParentDirCtrl',
controllerAs: 'mydir',
};
}
obj1 is populated on the child controller instance - that's why mydir.obj1 is undefined in the parent watcher. You can access obj1 directly via scope or by using the reference passed into the watcher:
$scope.$watch('mydir.obj1', function(val) {
$scope.mydir.obj1.occupation = 'Meteorologist';
// or
val.occupation = 'Meteorologis';
});
There is no scope inheritance here - both controllers operate on the same scope. Controller-AS syntax is what confuses you - I'd get rid of it to make things clearer.

How to inject variable into controller

I am trying to get started with angular.js but I can't figure out how to inject a simple variable into my controller before testing.
This is my controller:
angular.module("app").controller("SimpleController", SimpleController);
function SimpleController() {
var vm = this;
vm.myVar = 1;
vm.getMyVar = function() {
return vm.myVar;
};
}
My test looks like the following:
describe("SimpleController", function() {
var vm;
beforeEach(module('app'));
beforeEach(inject(function($controller, $rootScope) {
vm = $controller('SimpleController', {
$scope: $rootScope.$new(),
myVar: 2
});
}));
it('myVar should be 2 not 1', function() {
expect(vm.getMyVar()).toEqual(2);
});
});
I did a lot of google searching but this is really confusing because many people do use $scope in controller and not thisas I do. But I guess the injection of variables should work with this too?
You may want to try writing your controller this way. This will make $scope accessible.
(function () {
angular.module("app").controller("SimpleController", SimpleController);
SimpleController.$inject = ['$scope'];
function SimpleController($scope) {
$scope.somevar = "something";
}
})();
I'm sure you've probably came across the documentation, but here is a link to the docs which contains the basics and should at least get you going in the right direction.
Another alternative would be something like this:
app.controller('SimpleController', ['$scope', function($scope) {
$scope.somevar = "something";
....
}]);
When you use $scope, you're making that property publicly available in the view.

Angularjs require controller from directive

I'm trying to create a directive that will be used in multiple places in the app and I want it to opt into a controller.
When using the controller method
return {
...
controller: 'BlogDashCourseCtrl',
...
}
it gets the controller fine. But when I require it
return {
...
require: '^BlogDashCourseCtrl',
...
link: function($scope, iElm, iAttrs) {
$scope.title = iAttrs.title; // Do not share me with other directives
if($scope.title === $scope.step) { // $scope.step comes from a shared scope
...
}
}
}
it can't find the controller.
I don't want the controller to be called multiple times. I just want the directives to share a scope, have a private scope, too (so $scope in the directive doesn't bubble up) and do some stuff with a service.
A directive is attached to a DOM node. A DOM node can't have two scopes. Either you share the parent scope or you create an isolated one and explicitly inherit stuff from it.
BlogDashCourseCtrl:
this.step = 'whatever'; //maybe $scope.step
SomeDirective:
return {
...
require: '^BlogDashCourseCtrl',
...
link: function($scope, iElm, iAttrs, blogDashCourseCtrl) {
$scope.title = iAttrs.title; // Do not share me with other directives
if($scope.title === blogDashCourseCtrl.step) { // $scope.step comes from a shared scope
...
}
}
}
Notice that blogDashCourseCtrl does not reference the $scope of that directive/controller, but the reference itself.
There really is extensive documentation on this, with examples.

How to have a value passed to scope from outside the controller?

I am new to angular world and I have function which is loading the html inside perticular div on load and then controller gets initialize. I want to make single var available inside the controller so wondering if it's possible to assign that var to scope from outside the controller.
//controller
var cntlrs = angular.module('MyModule');
cntlrs.controller('ControllerTest', function ($scope, $http) {
//want to have var available here from $scope
});
//accessing scope from outside
var appElmt = document.querySelector('[ng-app=MyApp]');
var $scope = angular.element(appElmt).scope();
var customer = "New Customer";
//how can I set customer value inside scope?
I would suggest reading the angular docs more. $scope is your model (or probably the term ViewModel is more appropriate).
To get values into your controller, I would recommend a factory or a service. One can call setCustomer on the factory, then other controllers would be able to see that value using getCustomer.
var mod = angular.module('MyModule', []);
mod.factory("CustomerFactory", function () {
var customer;
return {
getCustomer: function () {
return custData;
}
setCustomer: function (custData) {
customer = custData;
}
}
});
mod.controller("TestController", function ($scope, $http, CustomerFactory) {
$scope.customer = CustomerFactory.getCustomer();
}
It might also be better if you weren't referencing $scope outside of angular (i.e. from angular.element(...).scope()). I don't know what you are trying to solve, but it seems like from the code above, all that logic can be put inside the controller.
Yes, from outside the controller you can target an element that is within your angular controller:
var scope = angular.element("#YourElementID").scope();
And now you will have access to everything on the scope (Just as if you were using $scope)
I decided to work like this, and it seems to be allright!! It does not require a big effort, the only boring part is that in the template you need always to use vars.somepropertyormethod
//an outside var that keeps all the vars I want in my scope
var vars = {
val1: 1,
val2: "dsfsdf",
val3: function() {return true;}
}
//and here I set the controller's scope to have ONLY vars and nothing else:
angular.module('myModule', [])
.controller('myControllerOpenToTheWorld', function($scope) {
$scope.vars = vars;
});
With this, I can set vars.anyproperty from anywhere I want. The trick is that the real values are all wrapped inside an object, so as long as you don't reassign the wrapper vars, you can access it from both outside and inside:
//change val2
vars.val2 = "new value changed from outside";
In the markup, it would work like this:
<div>{{vars.val1}}</div>
<div ng-if:"vars.val3()">{{vars.val2}}</div>

Mock action invoked on controller initialization in AngularJS in test

I'm having angularjs controller that basically looks like below
app.controller('MyCtrl', function($scope, service) {
$scope.positions = service.loadPositions(); // this calls $http internally
$scope.save = function() {
...
};
// other $scope functions here
});
Now every time I write test for any of the methods in $scope I need to stub service.loadPositions() like below:
it(should 'save modified position', function($controller, service, $rootScope) {
spyOn(service, 'loadPositions').andReturn(fakeData);
var scope = $rootScope.$new();
$controller('MyCtrl', {$scope: scope});
// test stuff here
})
Is there any way to avoid this first stubbing in every test? I mean If I already tested that this action is invoked on controller start, I don't really need stubbing this in every next test. This introduces a lot of repetition in each test.
EDIT
I stubmbled upon ngInit and I thought I could use it but it seems not to be recommended way do to such things, but I'm not sure why?
Use a beforeEach inside your describe function:
describe('My test', function() {
var $controller,
$rootScope,
serviceMock;
beforeEach(function() {
serviceMock = { loadPositions: function() {} };
spyOn(serviceMock, 'loadPositions').andReturn(fakeData);
inject(_$controller_, _$rootScope_) {
$rootScope = _$rootScope_.$new();
$controller = _$controller_('MyCtrl',
{$scope: $rootScope, service: serviceMock});
};
});
it('should save modified position', function() {
// test stuff here
});
});
Notice that I have moved the controller initialization to beforeEach as well so you don't have to do it again in every test.
Just in case you're wondering what the underscores in the inject arguments are for, they enable the test to declare a local $controller variable and a local $rootScope variable. Angular just ignores them when it's resolving the function dependencies.
Update: There was a little bug in the example code. Just fixed it.
You can move this into beforeEach() and use $provide to always return your fake service.
Not knowing all of your test code something like this should work.
var scope, controller;
beforeEach(module("app", function($provide){
var mockedService = {
loadPositions: function(){return fakeData;}//or use sinon
};
$provide.value('service', mockedService);
});
beforeEach(inject(function($rootScope, $controller) {
scope = $rootScope.$new();
controller = $controller;
}));
it(should 'save modified position', function() {
controller('MyCtrl', {$scope: scope});
// test stuff here
});

Categories

Resources