Angularjs require controller from directive - javascript

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.

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.

Sharing scope object with child directive scopes

I have a parent directive which creates an instance of a third party object during link and that variable needs to be accessible to child directives.
However, there are two limitations:
There can be multiple instances of this per-page, so a singleton at the top of the javascript file won't work.
The child directives are recursive, so they have to create their own scope.
The only way I can think of is to pass that value as an attribute to each child directive. That feels inefficient but given the above constraints, I don't see any alternative.
// Some imaginary third-party object
function Tree() {}
// Root directive which creates an instance of the object, links to the page, and loads the data needed.
app.directive('tree', function() {
return {
restrict: 'E',
replace: true,
template: '<div><nodes nodes="nodes"></nodes></div>',
scope: {
nodes: '='
},
link: function(scope, $element) {
// This value needs to be accessible to all child directives
scope.tree = new Tree();
}
};
});
// A directive to render an array of nodes
app.directive('nodes', function() {
return {
restrict: 'E',
replace: true,
template: '<ol>' +
'<li ng-repeat="node in nodes track by node.id">' +
'<node node="node"></node>' +
'</li>' +
'</ol>',
scope: {
nodes: '='
}
};
});
// A directive to render a single node, and recursively any child nodes
app.directive('node', ['$compile', function($compile) {
return {
restrict: 'E',
replace: true,
scope: {
node: '='
},
template: '<div><span ng-bind="node.text"></span></div>',
link: function(scope, $element) {
if (scope.node.children && scope.node.children.length > 0) {
console.log(scope.node.children);
var tmpl = '<nodes nodes="node.children"></nodes>';
var children = $compile(tmpl)(scope);
$element.append(children);
}
// #todo Here's a good example of where I need access to scope.tree
}
};
}]);
The only solution I can think of is adding tree: '=' to the scope objects and then passing in tree="tree" to each child.
Plunker
One solution would be to not isolate the scope, and that way the child directives would inherit from their parent scope. If the parent has scope.tree = new Tree() then the nested directives would inherit scope.tree.
However, since you mentioned that there can be multiple instances of this per page, that means you probably need an isolated scope (which is what you currently have). The only way to pass data into an isolated scope is through the directive attributes in the markup.
I think you answered your own question :)

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

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

How can I bind scope property of transcluded element to parent?

I'm attempting to create a reusable parent container directive. What I want is something like the following:
<parent title="Parent title">
<child></child>
</parent>
where I can have the child elements change a value in the parent's scope.
I've attempted this binding as follows (see this fiddle):
myApp.directive('parent', function() {
return {
scope: {'title': '#'},
transclude: true,
// Manually transclude so we set parent scope to be this scope.
link: function(scope, element, attrs, ctrl, transclude) {
transclude(scope.$new(), function(content) {
element.append(content);
});
},
restrict: 'EA',
template: '<div><b>PARENT:</b> {{title}}</div>',
controller: function($scope) {
$scope.inherited = true;
$scope.$watch('inherited', function(newValue, oldValue) {
if(!newValue) {
console.log('Parent: inherited switched');
}
});
}
}
});
myApp.directive('child', function() {
return {
restrict: 'EA',
scope:{
inherited: '='
},
template: '<div><b>Child:</b> inherited attribute = {{inherited}}</div>',
controller: function($scope) {
// Why is inherited not bound to parent here?
console.log($scope.inherited);
// It obviously exists in the parent...
console.log($scope.$parent.inherited);
}
}
});
From my understanding of the API, setting an object hash scope with inherited: '=' should bind to the parent property, but as you can see in the fiddle, it doesn't.
I have two questions:
Why is inherited not bound to the parent property?
How can I achieve this message passing?
Caveats:
I would prefer not to use require: '^parent', since this creates a rather strong dependency of the child on the parent (I might want to create a number of possible parent containers and a number of child directives and mix-and-match).
I absolutely will not use link in the child directive to climb to the $parent. That's a hack and won't work in general for deeply nested directives.
I am willing to use some hackery in the parent.
Thanks for any guidance.
You can either do:
<child inherited="inherited"></child>
where you actually pass in the inherited attribute, since that is what it is looking for when you do
scope:{
inherited: '='
},
Or you could do:
myApp.directive('child', function() {
return {
restrict: 'EA',
scope: false,
so that it uses it's parent's scope.
Either way, you are likely to run into some issues binding to a primitive. You will probably save yourself a lot of headache by making inherited an object, passing the object around, and when you need to display it show the object property:
JS:
$scope.inherited = {is: true};
HTML:
{{inherited.is}}
But then keep passing around the whole object.
http://jsfiddle.net/GQX9z/1/

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