Sharing scope object with child directive scopes - javascript

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 :)

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.

controllerAs in directive's link function not working

I created a d3 barchart directive using scope in the plunker http://plnkr.co/edit/yF8H9i8tyu1o2xJCN9bV
with controller having chartData in the scope.
.controller('d3Controller', ['$scope', function($scope) {
$scope.chartData = [10,20,30,40,50];
}])
and I have a bi-directional association of chartData with directive's isolated scope
scope: {
chartData: '='
},
restrict: 'EA',
replace: false,
link: function (scope, elem, attrs) {
var data = attrs.chartData.split(',');
var d3 = $window.d3;
var chart = d3.select(elem[0]);
chart
.append("div")
.attr("class", "chart")
//returns an array of all <div>...</div> elements in div
.selectAll("div")
.data(scope.chartData)
.enter()
.append("div")
.transition().ease("elastic")
.style("width", function (d) {
return d + "%"
})
.text(function (d) {
return d + "%"
});
and the associated directive is as follows
<bar-chart chart-data="chartData"></bar-chart>
This is working fine. But, I am trying to do the same using "controllerAs"
I tried making some changes, but, it is not working.
http://plnkr.co/edit/eIRkAtfJx9rlWN5LtllC
I changed the controller's scoped chartData to this
.controller('d3Ctrl', ['$scope', function($scope) {
var self = this;
self.chartData = [10,20,30,40,50];
}])
using controllerAs and bindToDirective options for directive
scope: { },
controllerAs: 'barCtrl',
controller: function() { },
bindToDirective: {
chartData: '='
},
when trying to get chartData, it is saying barCtrl is not defined.
.selectAll("div")
.data(barCtrl.chartData)
What am I doing wrong?
You are incorrectly doing some stuff in your controller As code.
1) You need to specify 2 way binding via the scope property of the settings not bindToDirective, there is no recognized property like that in the directive settings.
2) Need to use bindToController flag to specify any scope bound 2 way bound properties to be added to the controller instance and not directly on the scope. Though it is possible that you can do bindToController:{chartData:"="} it is not documented in the official doc and hence i would not recommend doing that way since it could be removed as well in the upcoming versions.
3) You can use the 4th argument to the link function as the controller instance and refer to it inside your linking function.
So it would look like
.directive('barChart', ['$window', function($window) {
var myDirective = {
controllerAs: 'barCtrl',
controller: angular.noop,
bindToController:true, //<-- Need to specify bound values to be added to the controller instance
scope: { //Need to use scope not bindToDirective
chartData: '='
},
restrict: 'EA',
replace: false,//if it is false you don't need it
//use the 4th argument as the controller instance
link: function (scope, elem, attrs, barCtrl) {
}
Demo

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 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/

How to read scope properties from parent scope

I am creating a simple directive however I am stuck at the beginning. My problem is that I am unable to read the property from scope in the post function. Here is my code:
<div mod type="{{subnet.isDynamic | type }}"></div>
networkInterfaces.directive('mod', function () {
return {
scope: {
type: '#'
},
link: function (scope, iElement, iAttrs, controller) {
console.log(scope.type);
if (scope.type == "Static") {
iElement.css('background', 'blue');
}
if (scope.type == "Dynamic") {
iElement.css('background', 'green');
}
}
}
});
Your scope property is really wrong and you don't need it here.
A working plnkr here : http://plnkr.co/edit/ezbzad?p=preview
Please refer to the docs for understand the scope property inside a directive : http://docs.angularjs.org/guide/directive
But if you really want to use just do that :
app.directive("mod", function(){
return {
scope: {
value: "#value"
},
link: function(scope, element, attrs){
console.log(scope.value);
}
}
});
the scope property with {} create a new isolate scope so you can't access to your value.
Adding value: "#value" the scope is bind to the value property but note that the syntax is a javascript object syntax it's :
value : "#value" and not value = "#value" !!!
Oh and a last thing : if your directive is call mod ... in your div your attribute is mod not someDirective.

Categories

Resources