angularjs inheriting scope in nested directives - angularjs

example in: http://jsfiddle.net/avowkind/PS8UT/
I want a nested child directive to get its data from its wrapping parent directive if present, otherwise from the outer controller.
<div ng-controller="MyCtrl">
<parent index="1">
<child></child>
</parent>
<parent index="2">
<child></child>
</parent>
<h1>No Parent</h1>
<child></child>
</div>
<hr>
Desired output
Parent 1
Child of parent 1
Parent 2
Child of parent 2
No Parent
Child of parent 0
Currently my child object only sees the outer controller value:
Actual output
Parent 1
Child of parent 0
Parent 2
Child of parent 0
No Parent
Child of parent 0
This is the simple version; in reality the outer directives get data from a server that is formatted by the nested child so what is communicated is a complex object not a simple string.
Furthermore the child is a visualisation that will work on different data sets so the outer parent directive is not always the same type.
More generally the pattern I am trying to get here is to have separate directives for populating the model and viewing it. so a more realistic usage would be
<temperature-for city="Auckland">
<plot/>
<analysis/>
</temperature-for>
<humidity-for city="Hamilton">
<plot/>
<analysis/>
</temperature-for>
<test-data>
<plot/>
</test-data>

A different approach which I personally have had great success using is to define the plot and analysis directives as isolate scopes, and then two-way bind the required input.
This way the directive are completely standalone components, with a explicit, defined interface. I personally made a plotting directive like this:
<plot data="countries['Auckland'].plot.data" options="countries['Auckland'].plot.options" legend="external" zoom="xy"></plot>
Scope would look like:
scope: {
data: '=',
options: '=',
zoom: '#?', // '?' indicates optional
legend: '#?',
}
This way there's no confusion what data is required for this component to work, and you can write documentation inside the directive for the desired input attributes.
All in all, this is a pattern which works very well for a large portion of use cases in AngularJS, i.e. whenever there is a case for reusability.
Edit:
Just wanted to add to that: Looking at your HTML, there's absolutely no indication what those directives use, they could depend on anything (e.g. do they get all the data from a service? or do they depend on a parent scope? If so, what scope?)

There are a couple different ways to do this but assuming that you truly want to use the parent scopes here is a solution to go along with your fiddle.
var myApp = angular.module('myApp', []);
function MyCtrl($scope) {
$scope.index = 0;
}
myApp.directive('parent', function () {
return {
transclude: true,
scope: {
index: '='
},
restrict: 'EA',
template: '<h2>Parent {{ index }}</h2>',
compile: function(tE, tA, transcludeFn) {
return function (scope, elem, attrs) {
elem.append(transcludeFn(scope)[1]);
};
}
}
});
myApp.directive('child', function () {
return {
restrict: 'EA',
scope: false,
template: '<p>Child of parent {{ index }}</p>'
}
});
You can see a fork of your fiddle here.
The idea is that by getting rid of the ngTranscludeDirective and manually creating the transclusion, you can link the transclusion with the scope of your choosing. Then you can append the result where ever you like in the element resulting from your directive's compilation.
The other main point is to make sure the child directive doesn't create a scope (at all, whether an isolate scope, transcluded scope, or new scope).
I think this will give you the results you're asking for.
NOTE: Study your scopes well, because tweaking these behaviors can have unexpected results.
For example, if you add a linking function to the child directive and it sets index to 5:
link: function(scope) {
scope.index = 5;
}
this will not affect scope.items for children nested in the parent. However it WILL affect the an external parent scope (in this case MyCtrl's scope). Any directive not inside a parent directive will just keep altering the MyCtrl index.
However if you add a new property to scope in the child link function:
link: function(scope) {
scope.somethingElse = foo;
}
scope.somethingElse will be available on the parent scope whether nested in a parent directive or not.

You'll need to transclude and fine grain it according to your parent directive scope, you can't do it using the ng-transclude directive: http://jsfiddle.net/PS8UT/2/
var myApp = angular.module('myApp', []);
function MyCtrl($scope) {
$scope.index = 0;
}
myApp.directive('parent', function ($compile) {
return {
scope: {
index: '#'
},
restrict: 'EA',
transclude: true,
template: '<h2>Parent {{ index }}</h2>',
link: function (scope, elem, attrs, ctrl, transclude) {
transclude(scope, function(clone, s){
elem.append($compile(clone)(s)); // basically you need to reassign the inner child scope to your current isolated scope
});
}
}
});
myApp.directive('child', function () {
return {
//scope: true, // no need to create another scope since you want to use the parent
// scope: { }, // no index printed
restrict: 'EA',
template: '<p>Child of parent {{ index }}</p>'
}
});
Usually, when you are dealing with templates and transclusion, needing parent inheritage, is a pain. ng-transclude won't use your immediate parent scope as the parent, it usually use your controller scope. It's stated in angular docs $compile docs:
transclude
compile the content of the element and make it available to the
directive. Typically used with ngTransclude. The advantage of
transclusion is that the linking function receives a transclusion
function which is pre-bound to the correct scope. In a typical setup
the widget creates an isolate scope, but the transclusion is not a
child, but a sibling of the isolate scope. This makes it possible for
the widget to have private state, and the transclusion to be bound to
the parent (pre-isolate) scope.

Related

AngularJS : Child directive scope to inherit from parent directive isolated scope?

If a and b are my directives such that b is a child element of a :
<a>
<b></b>
</a>
Is it possible that if a has an isolated scope, then b could inherit from it?
Example js:
app.directive('a', function () {
return {
restrict: 'E',
scope: {},
controller: function ($scope) {
$scope.prop1 = ...
}
}
});
app.directive('b', function () {
return {
restrict: 'E',
controller: function ($scope) {
//how to access $scope.prop1 here?
}
}
});
With this, I'm trying to make directives that are reusable and are supposed to be used as nested within each other.
I know that I can require the controller of a on directive b to access it within the link function of b as one way to share the data between controllers, but that approach isn't working very well if I have more than one level of nesting.
This is where you need to use the manual transclusion function. If the parent directive has an isolate scope, the child DOM elements (and their directives) would not inherit from it (only, if they were in its template).
When you transclude, you can specify the scope explicitly:
.directive("a", function(){
return {
scope: {},
transclude: true,
link: function(scope, element, attrs, ctrls, transclude){
var newScope = scope.$new();
transclude(newScope, function(clone){
element.append(clone);
})
}
};
});
You should note, though, that although the above would work (in the sense that the child directive's scope would inherit the parent's isolate scope), it is also a somewhat confusing experience to the user of your directive.
To see why, imagine that a exposes some $innerProp on its scope. The user of a now has to know that such property is "magically" available. This makes the HTML less readable without knowing a lot about a:
<a>
<b item="$innerProp"></b>
</a>
Addendum
Depending on your use case, there might be other approaches that are more suitable. The above approach works better when a and b are independent, and when a uses its contents to allow its user to specify some template.
If b is only (or mostly) used as a child of a, then it should require it. a can expose whatever it needs via its controller API to b.
Lastly, if a has a well-defined structure, then it should use its template to specify b. In your example, this could easily be achieved with a template.

Why doesn't scope pass through properly to nested directives?

In this example plunker, http://plnkr.co/edit/k2MGtyFnPwctChihf3M7, the nested directives compile fine when calculating the DOM layout, but error when the directive tries to reference a variable to bind to and says the variable is undefined. Why does this happen? The data model I am using is a single model for many nested directives so I want all nested directives to be able to edit the top level model.
I havn'et got a clue as to what you're trying to do. However, your comment 'so I want all nested directives to be able to edit the top level model' indicates you want your directive to have scope of your controller. Use
transclude = true
in your directive so that your directives can have access to your the parent scope.
http://docs.angularjs.org/guide/directive#creating-a-directive-that-wraps-other-elements
I don't know why you are doing it this way exactly, it seems like there should be a better way, but here goes a stab at getting your code working. First you create an isolated scope, so the scopes don't inherit or have access to anything but what is passed in the data attribute. Note that you can have your controller set dumbdata = ... and say <div data="dumbdata" and you will only have a data property on your isolated scope with the values from dumbdata from the parent in the data property. I usually try to use different names for the attribute and the data I'm passing to avoid confusion.
app.directive('project', function($compile) {
return {
template: '<div data="data"></div>',
replace: true,
scope: {
data: '=' // problem
},
Next, when you compile you are passing variables as scopes. You need to use real angular scopes. One way is to set scope: true on your directive definition, that will create a new child scope, but it will inherit from the parent.
app.directive('outer', function($compile) {
var r = {
restrict: 'A',
scope: true, // new child scope inherits from parent
compile: function compile(tEle, tAttr) {
A better way is probably to create the new child scope yourself with scope.$new(), and then you can add new child properties to pass for the descendants, avoiding the problem of passing values as scopes and still letting you have access to the individual values you're looping over (plunk):
app.directive('outer', function($compile) {
var r = {
restrict: 'A',
compile: function compile(tEle, tAttr) {
return function postLink(scope,ele,attrs) {
angular.forEach(scope.outer.middles, function(v, i) {
var x = angular.element('<div middle></div>');
var s = scope.$new(); // new child scope
s.middle = v; // value to be used by child directive
var y = $compile(x)(s); // compile using real angular scope
ele.append(y);
});
};
}
};
return r;
});

Communication between nested directives

I'm trying to communicate between a parent directive and its nested child directive, and the other way. I've managed to achieve this by using $broadcast and $emit, but because I'm passing in some arguments to the directives I've had to create isolated scope on my directives, so in order for the $broadcast/$emit to work I have to broadcast 'up a level' on the parent scope (scope.$parent.$broadcast).
Now the broadcast is no longer just going to the nested child, but to all directives at the same level, which I don't want. I've created a plunker to show the issue, here.
What I need is for when one of the buttons is pressed, only the child directive to receive the broadcast message, and vice-versa. Am I missing something, or is this not possible when using isolated scope?
In my HTML:
<body ng-app="myApp">
<directive1 data-title="Click me to change name">
<directive2 data-name="John Smith"></directive2>
</directive1>
<directive1 data-title="Click me to change this other name">
<directive2 data-name="Gordon Freeman"></directive2>
</directive1>
</body>
Directive 1:
<div>
<button ng-click="changeName()">{{title}}</button>
<div ng-transclude></div>
</div>
Directive 2:
<div>
<h2>{{name}}</h2>
</div>
My directives:
myApp.directive('directive1', function(){
return {
restrict: 'E',
replace: true,
templateUrl: 'Directive1.html',
transclude: true,
scope: {
title: '#'
},
link: function(scope, elem){
scope.changeName = function() {
scope.$parent.$broadcast('ChangeName');
};
scope.$parent.$on('NameChanged', function(event, args){
scope.title = 'Name changed to ' + args;
});
}
}
});
myApp.directive('directive2', function(){
return {
restrict: 'E',
replace: true,
templateUrl: 'Directive2.html',
scope: {
name: '#'
},
link: function(scope, elem){
scope.$on('ChangeName', function(event, args){
scope.name = 'Adam West';
scope.$emit('NameChanged', 'Adam West');
});
}
}
});
There are 5 main ways to communicate between directives:
1) A common service.
This is not a good solution for you because services are always singletons and you want to have multiple unique directives. You could only use a service if you maintained a dictionary of parents and children in the service and managed routing the calls to the right associated parent / child, which is the same problem you have using events.
2) Events.
If you can't limit the event to the correct part of the DOM / tree via broadcasting from the right node, you'll have to add a unique identifier to the event. In this case, if you are broadcasting from the root and / or multiple children are receiving the message, give each parent / child a unique identifier and add this to the emit / broadcast / on. This is not a pretty solution, but it will work.
3) One-way bindings.
The '&' binding in an isolated scope lets you pass parent functions into a child. The child can then call these functions on the parent scope to notify the parent of changes. This is great for child - parent communication but doesn't go the other way. You could combine this solution with broadcasting an event from the parent to communicate to the child.
4) Two-way bindings.
Sometimes you can use a attribute on an isolated scope to pass information or flags back and for between the parent-child. This doesn't work in your example because the parent doesn't know anything about his child since the child is injected via transclusion.
5) require parent controller.
A directive can use the require property to specify that another directive is required to exist either as a parent or on the same element. You can't require sibling directives. The required directives must have controllers defined. The controller is then passed to the link (or compile) function and you can call functions on the controller. This can be used to allow communication between the directives. In your example, if directive2 required directive1 you could set functions like addChild() to the controller. The child (directive2) would then add itself to the parent who could update / call all children when changeName was called.
myApp.directive('directive1', function(){
return {
// ...
controller: function($scope) {
$scope.children = [];
this.addChild = function(child) {
$scope.children.push(child);
}
},
link: function(scope, elem){
scope.changeName = function() {
_.each(scope.children, function(child) {
child.setName('Adam West');
};
};
},
}
});
myApp.directive('directive2', function(){
return {
// ...
require: "^directive1", // require directive1 as a parent or on same element
link: function(scope, elem, attributes, directive1Controller){
child = {
setName: function(name) {
scope.name = name;
},
};
directive1Controller.addChild(child);
}
}
});

AngularJS : Child input directive needs to compile in the scope of its parent for ng-model to bind

We have a contact form we use in many applications. There are many default values, validation rules, structure, etc, that are repeated. We're working on a set of directives in order to make the view more semantic and less verbose.
There are a few targets we're shooting for.
Defining the contact form model once in a parent directive like this: <div my-form model='formModel'>. Associated children directives would be able to get the base model from the model attribute.
Supply the default configuration (size, validation rules, placeholders, classes, etc) for each input, but allow the possibility for attributes to be overwritten if necessary. Thus, we are creating child directives using the my-form directive's controller for communication. We also want these child directives to bind to the application controller's model formModel.
I'm having some trouble with implementing this.
formModel is exposed through the parent directive's controller, but I'm having to manually $compile the child directive using scope.$parent in the link function. This seems smelly to me, but if I try to use the child directive's scope the compiled HTML contains the correct attribute (it's visible in the source), but it isn't bound to the controller and it doesn't appear on any scope when inspected with Batarang. I'm guessing I'm adding the attribute too late, but not sure how to add the attribute earlier.
Although I could just use ng-model on each of the child directives, this is exactly what I'm trying to avoid. I want the resulting view to be very clean, and having to specify the model names on every field is repetitive and error-prone. How else can I solve this?
Here is a jsfiddle that has a working but "smelly" setup of what I'm trying to accomplish.
angular.module('myApp', []).controller('myCtrl', function ($scope) {
$scope.formModel = {
name: 'foo',
email: 'foo#foobar.net'
};
})
.directive('myForm', function () {
return {
replace: true,
transclude: true,
scope: true,
template: '<div ng-form novalidate><div ng-transclude></div></div>',
controller: function ($scope, $element, $attrs) {
$scope.model = $attrs.myModel;
this.getModel = function () {
return $scope.model;
};
}
};
})
.directive('myFormName', function ($compile) {
return {
require: '^myForm',
replace: true,
link: function (scope, element, attrs, parentCtrl) {
var modelName = [parentCtrl.getModel(),attrs.id].join('.'),
template = '<input ng-model="' + modelName + '">';
element.replaceWith($compile(template)(scope.$parent));
}
};
});
There is a much simpler solution.
Working Fiddle Here
Parent Form Directive
First, establish an isolated scope for the parent form directive and import the my-model attribute with 2-way binding. This can be done by specifying scope: { model:'=myModel'}. There really is no need to specify prototypical scope inheritance because your directives make no use of it.
Your isolated scope now has the 'model' binding imported, and we can use this fact to compile and link child directives against the parent scope. For this to work, we are going to expose a compile function from the parent directive, that the child directives can call.
.directive('myForm', function ($compile) {
return {
replace: true,
transclude: true,
scope: { model:'=myModel'},
template: '<div ng-form novalidate><div ng-transclude></div></div>',
controller: function ($scope, $element, $attrs) {
this.compile = function (element) {
$compile(element)($scope);
};
}
};
Child Field Directive
Now its time to setup your child directive. In the directive definition, use require:'^myForm' to specify that it must always reside within the parent form directive. In your compile function, add the ng-model="model.{id attribute}". There is no need to figure out the name of the model, because we already know what 'model' will resolve to in the parent scope. Finally, in your link function, just call the parent controller's compile function that you setup earlier.
.directive('myFormName', function () {
return {
require: '^myForm',
scope: false,
compile: function (element, attrs) {
element.attr('ng-model', 'model.' + attrs.id);
return function(scope, element, attrs, parentCtrl) {
parentCtrl.compile(element);
};
}
};
});
This solution is minimal with very little DOM manipulation. Also it preserves the original intent of compiling and linking input form fields against the parent scope, with as little intrusion as possible.
It turns out this question has been asked before (and clarified) here, but never answered.
The question was also asked on the AngularJS mailing list, where the question WAS answered, although the solution results in some smelly code.
Following is Daniel Tabuenca's response from the AngularJS mailing list changed a bit to solve this question.
.directive('foo', function($compile) {
return {
restrict: 'A',
priority: 9999,
terminal: true, //Pause Compilation to give us the opportunity to add our directives
link: function postLink (scope, el, attr, parentCtrl) {
// parentCtrl.getModel() returns the base model name in the parent
var model = [parentCtrl.getModel(), attr.id].join('.');
attr.$set('ngModel', model);
// Resume the compilation phase after setting ngModel
$compile(el, null /* transclude function */, 9999 /* maxPriority */)(scope);
}
};
});
Explanation:
First, the myForm controller is instantiated. This happens before any pre-linking, which makes it possible to expose myForm's variables to the myFormName directive.
Next, myFormName is set to the highest priority (9999) and the property of terminal is set true. The devdocs say:
If set to true then the current priority will be the last set of directives which will execute (any directives at the current priority will still execute as the order of execution on same priority is undefined).
By calling $compile again with the same priority (9999), we resume directive compilation for any directive of a lower priority level.
This use of $compile appears to be undocumented, so use at your own risk.
I'd really like a nicer pattern for follow for this problem. Please let me know if there's a more maintainable way to achieve this end result. Thanks!

dynamic directives in angularjs

The directive's attributes don't change when the scope is updated, they still keep the initial value. What am I missing here?
HTML
<ul class="nav nav-pills nav-stacked" navlist>
<navelem href="#!/notworking/{{foo}}"></navelem>
<navelem href="#!/working">works great</navelem>
</ul>
<p>works: {{foo}}</p>
Javascript
(based on angular tabs example on front-page)
angular.module('myApp.directives', []).
directive('navlist', function() {
return {
scope: {},
controller: function ($scope) {
var panes = $scope.panes = [];
this.select = function(pane) {
angular.forEach(panes, function(pane) {
pane.selected = false;
});
pane.selected = true;
}
this.addPane = function(pane) {
if (panes.length == 0)
this.select(pane);
panes.push(pane);
}
}
}
}).
directive('navelem', function() {
return {
require: '^navlist',
restrict: 'E',
replace: true,
transclude: true,
scope: { href: '#href' },
link: function(scope, element, attrs, tabsCtrl) {
tabsCtrl.addPane(scope);
scope.select = tabsCtrl.select;
},
template:
'<li ng-class="{active: selected}" ng-click="select(this)"><a href="{{href}}" ng-transclude></a></li>'
};
});
By defining scope: {} in your directive, it is creating a isolated scope.
So the parent scope is now invisible from the directive.
If you want to refer the parent scope, then you can put scope: true for shared
scope (among same directives) and omit the scope declaration for just normal scope nesting.
Or if you want to just refer $scope.foo of the parent, you can define
explicit scope variables like you've done in the child directive.
There are three types of directive scope inheritance:
No 'scope: ...' or explicit scope: false - no new scope is created. The directive uses the same scope as the parent. This is simple and convenient, but if you are building reusable components, this is not recommended, since the directive will likely only be usable if the parent scope has certain scope properties defined that the directive needs to use/access.
scope: true - creates a new scope, shared by all directives on the same element, with normal prototypical inheritance of the parent scope. Again, probably not the best choice for reusable components, since the directive probably shouldn't have access to the parent scope properties -- it could accidentally change something in the parent.
scope: { ... } - creates a new "isolated" scope -- it does not prototypically inherit from the parent scope. However, the object hash ( i.e., the { ... } ) allows us to define local directive scope properties that are derived from the parent scope -- so we can control which properties are shared, and how.
Use '=' for powerful 2-way binding between a parent scope property and a directive scope property -- changes to either scope property affect the other.
Use '#' for binding a parent's attribute value to a directive scope property. This is essentially 1-way binding. Only parent scope changes affect the directive scope.
Use '&' to bind to parent scope expressions/functions.
For your particular problem, you need to indicate in the object hash which scope properties you want to have 2-way binding.
For more about directive scopes (including pictures), please see section directives here: What are the nuances of scope prototypal / prototypical inheritance in AngularJS?
Like Mark Rajcok said - scope: {} will create a new isolated scope that don't inherit properties from parent, however we still can get access to these properties by using $parent property.
Controller:
app.controller('indexController', function($scope) {
$scope.test="Hello world!";
});
Directive
app.directive("test", function() {
return{
restrict: "A",
scope: {},
controller: function($scope){
console.log("directiv $scope.$parent.test: " + $scope.$parent.test);
console.log("directiv $scope.test: " + $scope.test);
}
};
});
output:
directiv $scope.$parent.test: Hello world!
directiv $scope.test: undefined

Resources