Multiple directives [ngController, ...] asking for new/isolated scope - angularjs

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

Related

Why does my nested directive disconnect from its ngModel as soon as $setViewValue() is called?

Plunker here.
I have a directive ("child") nested inside another directive ("parent"). It requires ngModel, and ngModelCtrl.$modelValue is shown and kept up-to-date just fine in its template. That is, until I call ngModelCtrl.$setViewValue().
So here is the HTML initialising the directives:
<div parent>
<div child ng-model="content">Some</div>
</div>
And here are the directives:
angular.module('form-example2', [])
.controller('MainCtrl', function($scope){
$scope.content = 'Hi';
})
.directive('parent', function() {
return {
transclude: true,
template: '<div ng-transclude></div>',
controller: function(){
},
scope: {}
};
})
.directive('child', function() {
return {
require: ['ngModel', '^parent'],
transclude: true,
template: '<div>Model: {{model.$modelValue}} (<a style="text-decoration: underline; cursor: pointer;" ng-click="alter()">Alter</a>)<br />Contents: <div style="background: grey" ng-transclude></div></div>',
scope: {},
link: function(scope, elm, attrs, ctrl) {
var ngModelCtrl = ctrl[0];
var parentCtrl = ctrl[1];
scope.model = ngModelCtrl;
// view -> model
scope.alter = function(){
ngModelCtrl.$setViewValue('Hi2');
}
// model -> view
// load init value from DOM
}
};
});
When the model (i.e. content) changes, this change can be seen inside the child directive. When you click the "Alter" link (which triggers a call of $setViewValue()), the model's value should become "Hi2". This is correctly displayed inside the child directive, but not in the model outside the directive. Furthermore, when I now update the model outside the directive, it is no longer updated inside the directive.
How come?
The directives ended up being just fine; the only problem was that the passed model should be an object property. Hence, the directives work if the following modifications are made to the calling code (Plunker):
In the controller, instead of $scope.content = 'Hi';:
$scope.content = {
value: 'Hi'
};
In the template, replace all references to content with content.value:
<input ng-model="content.value" type="text" />
<div parent>
<div child ng-model="content.value">Some</div>
</div>
<pre>model = {{content.value}}</pre>
The reason this works, roughly, is that when Angular passes the reference to the model to the transcluded scope of the parent directive (i.e. the one the child is in), this is only a reference when it refers to an object property - otherwise it is a copy, which Angular cannot watch for changes.
#Josep's answer helped greatly so, even though it did not provide the actual solution, if you're reading this and it's useful, give it a vote :)

Being wrapped by a directive, how can I access its scope?

How can I access the directive's isolate scope in the directive's body? My DOM looks like this:
<div ng-app="app">
<directive>
<p>boolProperty: {{boolProperty|json}}</p>
</directive>
</div>
The boolProperty is assigned inside the directive's link function:
angular.module("app", []).directive("directive", function() {
return {
scope: {},
link: function($scope) {
$scope.boolProperty = true;
}
};
});
The problem is, the child <p> inside the directive binds to the directive's parent scope, not the directive's isolated scope. How can I overcome this?
Click here for jsFiddle.
There are couple of problems in your code.
The default restrict option is A for attribute so anyways your directive will not be compiled because you are using it as an element. Use restrict: 'E' to make it work.
As per the documentation, the scope of the transcluded element is not a child scope of the directive but a sibling one. So boolProperty will always be undefined or empty. So you have to go up the scope level and find the proper sibling.
<div ng-app="app">
<directive>
<p>boolProperty: {{$parent.$$childHead.boolProperty}}</p>
</directive>
</div>
and need to use transclusion in the directive as:
angular.module("app", []).directive("directive", function() {
return {
restrict: 'E',
scope: {},
transclude: true,
template: '<div ng-transclude></div>',
link: function(scope) {
scope.boolProperty = true;
}
};
});
However, this approach is not advisable and break later If you add a new controller before the directive because transcluded scope becomes 2nd sibling unlike 1st as before.
<div ng-app="app">
<div ng-controller="OneCtrl"></div>
<directive>
<p>boolProperty: {{$parent.$$childHead.boolProperty || $parent.$$childHead.$$nextSibling.boolProperty}}</p>
</directive>
</div>
Here is the Working Demo. The approach I mentioned is not ideal so use at your own risk. The #CodeHater' s answer is the one you should go with. I just wanted to explain why it did not work for you.
You forgot about two things:
By default AngularJS uses attrubute restriction, so in your case in directive definition you should specify restrict: "E"
You should use child scope, but not isolated. So set scope: true to inherit from parent view scope.
See updated fiddle http://jsfiddle.net/Y9g4q/1/.
Good luck.
From the docs:
As the name suggests, the isolate scope of the directive isolates everything except models that you've explicitly added to the scope: {} hash object. This is helpful when building reusable components because it prevents a component from changing your model state except for the models that you explicitly pass in.
It seems you would need to explicitly add boolProperty to scope.
<div ng-app="app" ng-controller="ctrl">
<directive bool="boolProperty">
<p>boolProperty: {{boolProperty|json}}</p>
</directive>
</div>
JS
angular.module("app", []).controller("ctrl",function($scope){
$scope.boolProperty = false;
}).directive("directive", function() {
return {
restrict:"E",
scope: {boolProperty:'=bool'},
link: function($scope) {
$scope.boolProperty = "i'm a boolean property";
}
};
});
Here's updated fiddle.

Directive's isolated scope variables are undefined if it is wrapped in a directive which has in its template ng-if and tranclusion enabled

I am facing is an issue which is demonstrated in the following example:
http://jsfiddle.net/nanmark/xM92P/
<div ng-controller="DemoCtrl">
Hello, {{name}}!
<div my-wrapper-directive>
<div my-nested-directive nested-data="nameForNestedDirective"></div>
</div>
</div>
var myApp = angular.module('myApp', []);
myApp.controller('DemoCtrl',['$scope', function($scope){
$scope.name = 'nadia';
$scope.nameForNestedDirective = 'eirini';
}])
.directive('myWrapperDirective', function(){
return {
restrict: 'A',
scope: {},
transclude: true,
template: "<div ng-if='isEnabled'>Hello, <div ng-transclude></div></div>",
link: function(scope, element){
scope.isEnabled = true;
}
}
})
.directive('myNestedDirective', function(){
return {
restrict: 'A',
scope: {
nestedData: '='
},
template: '<div>{{nestedData}}</div>',
};
});
I want to create a directive (myWrapperDirective) which will wrap many other directives such as 'myNestedDirective of my example.
'myWrapperDirective' should decide if its content will be displayed or not according to ng-if expression's value, but if contents is a directive like 'myNestedDirective' with an isolated scope then scope variable 'nestedData' of 'myNestedDirective' is undefined.
The problem is with the double-nested isolated scopes. You see, you are using the nameForNestedDirective variable, defined in the outer scope from the inner scope which is isolated. This means it does not inherit this variable, thus undefined is passed to the nested directive.
A diagram to explain:
Outer scope - DemoCtrl
- Defines: name
- Defines: nameForNestedDirective
+ Uses: name
Inner isolated scope 1 - myWrapperDirective
- Defines: (nothing)
- Inherits: (NOTHING! - It is isolated)
+ Uses: (nothing)
* Passes nestedData=nameForNestedDirective to nested directive, but
nameForNestedDirective is undefined here!
Inner isolated scope 2 - myNestedDirective
- Defines: nestedData (from scope definition)
- Inherits: (NOTHING! - It is isolated)
+ Uses nestedData
You can convince yourself this is the case by commenting out the scope definition of the wrapper directive ("hello eirini" is displayed as expected):
.directive('myWrapperDirective', function(){
return {
...
//scope: {},
...
I am not sure if the wrapper directive really needs to have an isolated scope. If it doesn't, maybe removing the isolated scope will solve your problem. Otherwise you will have either to:
Pass the data first to the wrapper and then to the nested directives
Pass the data to the wrapper directive, write a controller for it that exposes the data and then require the wrapper controller from the nested directive.

AngularJS directive transclude part binding

I'd like to use a directive, transclude content, and call directive's controller method within the transcluded part:
<mydirective>
<div ng-click='foo()'>
click me
</div>
</mydirective>
app.directive "mydirective", ->
return {
restrict: 'EACM',
transclude: true
template: "<div ng-transclude></div>"
scope: { } #required: I use two way binding on some variable, but it's not the question here
controller: [ '$scope', ($scope)->
$scope.foo = -> console.log('foo')
]
}
plunkr here.
How can I do that please?
I have a different answer, which is not a hack and I hope it will be accepted..
see my plunkr for a live demo
Here is my usage of the directive
<div custom-directive custom-name="{{name}}">
if transclude works fine you should see my name right here.. [{{customName}}]
</div>
Note I am using customName within the directive and I assign it a value as part of the directive's scope.
Here is my directive definition
angular.module('guy').directive('customDirective', function($compile, $timeout){
return {
template : '<div class="custom-template">This is custom template with [{{customName}}]. below should be appended content with binding to isolated scope using the transclude function.. wait 2 seconds to see that binding works</div>',
restrict: 'AC',
transclude: true,
scope : {
customName : '#'
},
link : function postLink( scope, element, attrs, dummy, transcludeFn ){
transcludeFn( scope, function(clone, innerScope ){
var compiled = $compile(clone)(scope);
element.append(compiled);
});
$timeout( function(){
scope.customName = 'this stuff works!!!';
}, 2000);
}
}
});
Note that I am changing the value on the scope after 2 seconds so it shows the binding works.
After reading a lot online, I understood the following:
the ng-transclude directive is the default implementation to transclusion which can be redefined per use-case by the user
redefining a transclusion means angular will use your definition on each $digest
by default - the transclusion creates a new scope which is not a child of the isolated scope, but rather a sibling (and so the hack works). If you redefine the transclusion process you can choose which scope is used while compiling the transcluded content.. -- even though a new scope is STILL created it seems
There is not enough documentation to the transclude function. I didn't even find it in the documentation. I found it in another SO answer
This is a bit tricky. The transcluded scope is not the child of the directive scope, instead they are siblings. So in order to access foo from the ng-click of the transcluded element, you have to assign foo to the correct scope, i.e. the sibling of the directive scope. Be sure to access the transcluded scope from the link function because it hasn't been created in controller function.
Demo link
var app = angular.module('plunker', []);
app.directive("mydirective", function(){
return {
transclude: true,
restrict: 'EACM',
template: "<div> {{ name }} <br/><br/> <div ng-transclude> </div></div>",
scope: { },
link: function($scope){
$scope.name = 'Should change if click below works';
$scope.$$nextSibling.foo = function(){
console.log('foo');
$scope.name = 'it works!';
}
}
}
})
Another way is assigning foo to the parent scope because both prototypally inherits from the parent scope, i.e.
$scope.$parent.foo = ...
Technically, if you remove scope: { }, then it should work since the directive will not create an isolated scope. (Btw, you need to add restrict: "E", since you use the directive as element)
I think it makes more sense to call actions defined in parent scope from directive rather than call the actions in the directive from parent scope. Directive should be something self-contained and reusable. The actions in the directive should not be accessible from outside.
If you really want to do it, you can try to emit an event by calling $scope.$broadcast(), and add a listener in the directive. Hope it helps.

How can I inherit complex properties from the parent scope into my directive's isolated scope

After reviewing AngularJS (and related) documentation and other stackoverflow questions regarding isolated scopes within directives, I'm still a little confused. Why can't I do a bi-directional binding between the parent scope and directive isolated scope, where the parent scope property is an object and not an attribute? Should I just use the desired property off scope.$parent? That seems wrong. Thanks in advance for your help.
The related fiddle is here.
HTML:
<div ng-app="myApp">
<div ng-controller="myCtrl">
<div my-directive>{{test.name}}</div>
</div>
</div>
JavaScript:
var myApp = angular.module('myApp', []);
myApp.controller('myCtrl', function ($scope) {
$scope.test = {name:"name", value:"value"};
});
myApp.directive("myDirective", function () {
return {
replace: true,
restrict: 'A',
scope: {test: '='},
template: '<div class="parent"><div>This is the parent Div.</div><div>Value={{test}}</div></div>',
link: function (scope, element, attrs) {
console.log("scope.test=["+scope.test +"]");
console.log("scope.$parent.test=["+scope.$parent.test.name+"]");
}
};
});
For directives using an isolate scope, attributes are used to specify which parent scope properties the directive isolate child scope will need access to. '=' provides two-way binding. '#' provides "one-way strings". '&' provides one-way expressions.
To give your directive (two-way binding) access to parent scope object property test, use this HTML:
<div my-directive test="test"></div>
It might be more instructive to use different names:
<div my-directive some-obj-prop="test"></div>
Then in your directive:
scope: { localDirProp: '=someObjProp'},
template: '<div ...>Value={{localDirProp}}...',
Isolate scopes do not prototypically inherit from the parent scope, so it does not have access to any of the parent scope's properties (unless '#' or '=' or '&' are used). Using $parent is a way to still access the parent scope, but not via prototypical inheritance. Angular creates this special $parent property on scopes. Normally (i.e. best practice), it should not be used.

Resources