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);
}
}
});
Related
I have the following (https://jsfiddle.net/hhgqup4f/5/):
<div parent parent-model="vm.model1" ng-controller="Controller as vm">
Parent
</div>
And the controller is:
app.controller("Controller", Controller);
function Controller() {
var vm = this;
vm.model1 = "My Model 1";
vm.model2 = "My Model 2";
}
And then I have a directive as follows:
app.directive("parent", parent);
function parent() {
var parent = {
controller: ["$scope", controller],
replace: false,
restrict: "A",
scope: {
model: "="
}
};
return parent;
function controller($scope) {
console.log($scope.model);
}
}
With parent-model="vm.model1" I am trying to say which property from the controller should be used by the directive. But when I do console.log($scope.model) in the directive I get the error:
"Error: [$compile:multidir] Multiple directives [ngController, parent (module: app)] asking for new/isolated scope on: <div parent="" parent-model="vm.model1" ng-controller="Controller as vm">
How to solve this?
The error ...
"Error: [$compile:multidir] Multiple directives [ngController, parent (module: app)] asking for new/isolated scope on: <div parent="" parent-model="vm.model1" ng-controller="Controller as vm">
... is quite illustrative as AngularJS doesn't allow multiple directives (on the same DOM level) to create their own isolate scopes.
According to the documentation, this restriction is imposed in order to prevent collision or unsupported configuration of the $scope objects.
Normally a directive is supplied with an isolate scope with an intention towards componentization or reuse of some logic/action attached to the DOM.
Therefore, it makes sense that two reusable components cannot be merged together to produce a combined effect (at least in AngularJS).
Solution
Change your directive usage such that it is supplied with the required properties from its immediate parent (which in this case is the ngController directive).
<div ng-controller="Controller as vm">
<div parent parent-model="vm.model1"></div>
</div>
Similarly, you can access the passed properties to the isolate scope of the directive in their normalized format:
app.directive('parent', function(){
return {
scope: {
parentModel: '=' // property passed from the parent scope
},
controller: function($scope){
console.log($scope.parentModel);
}
};
});
Demo
Directive to Directive Communication
Two or more directives with isolate scopes, as mentioned earlier, can't be used on the same DOM element. However, it is possible for one of the directives to have an isolated scope. Other directives, in this case, can communicate if required by requireing its controller as such:
<div dir-isolate dir-sibling></div>
...
.directive('dirIsolate', function(){
return {
scope: {},
controller: function(){
this.askSomething = function(){
return 'Did you ask for something?';
};
}
};
})
.directive('dirSibling', function(){
return {
require: 'dirIsolate', // here is the trick
link: function(scope, iElement, attrs, dirSiblingCtrl){ // required controller passed to the link function as fourth argument
console.log(dirSiblingCtrl.askSomething());
}
};
});
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.
I had such html code
<a href="#" id="someLink">
<i class="someClass"></i><span>Some span text</span>
</a>
<div id="dependsOnSomeLink">
<!-- something here -->
</div>
that I wanted to separate into directives in AngularJS. The logic behind it was, that contents of div is hidden by default and is shown by click on a element.
I wanted to separate that into two directives like that:
angular.module('someModule', [])
.directive('prAbc', function($log) {
return {
require: 'prDef',
restrict: 'E',
template: '<i class="someClass"></i><span>Some span text</span>',
controller: function($scope, $element, $attrs, $transclude) {
// do the harlem share <-- here
},
link: function(scope, iElement, iAttrs, prDef, transcludeFn) {
iElement.bind('click', function() {
prDef.toggleDependentBlock();
});
}
};
})
.directive('prDef', function($log)
{
return {
restrict: 'E',
template: '<div id="dependsOnSomeLink"></div>',
controller: function($scope, $element, $attrs, $transclude) {
$scope.showDependentBlock = false;
this.toggleDependentBlock = function() {
$scope.showDependentBlock = false === $scope.showDependentBlock;
};
}
};
});
and later use it like that
<pr-abc></pr-abc>
<pr-def></pr-def>
But prDef is not defined when it is called from prAbc directive.
require: 'prDef' works when prAbc and prDef are applied on the same DOM element, which is not your case.
What you may need is "parent" directive which will act as a router between the two communicating directives. prDef registers itself to the parent controller and prAbc calls the parent controller which, in turn, calls prDef.
Check out this plucker.
There are at least 3 other options to consider:
Have the parent controller listen to specific events that prAbc emits and broadcasts them downwards, where prDef listens on.
Have the parent controller pass callback functions ('&') to the child directives for registration and routing.
All functions and data is defined on the parent and passed to child directives through binding or prototypal inheritance (essentially, ruin your design :P).
For all 3 options, the parent controller does not have to be defined in a directive, it can be any controller which you apply like:
<div ng-contoller="ParentCtrl">
<pr-abc></pr-abc>
<pr-def></pr-def>
</div>
Your example does not provide enough context to reason which option is the best, but I think the parent directive should be the preferred way to go.
If it helps, checkout how ui-bootstrap's tabs and accordion are implemented. It's a very helpfull example of collaborating, still decoupled, set of directives.
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.
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!