Directive with root element with ngRepeat and replace: true - angularjs

Can someone explain the root cause behind the following behavior?
If a directive with isolate scope scope: {} has a root element with ng-repeat AND replace: true then it "breaks" the isolate scope, meaning that the isolate scope is not accessible/visible from within the directive, and the directive starts receiving variables from the outer scope.
Here's the minimally reproducible example I could make:
app.controller('MainCtrl', function($scope) {
$scope.name = 'MainCtrl';
});
app.directive("foo", function(){
return {
replace: true,
scope: {},
template: "<div ng-repeat='item in [1]'>{{name}}</div>",
controller: function($scope){
$scope.name = "foo";
}
};
});
The following view would render "MainCtrl":
<div ng-controller="MainCtrl">
<foo></foo>
</div>
Adding either a non-ng-repeat-able root to the template OR setting replace: false renders the expected result "foo".
Plunker

It does not just happen for ng-repeat, this seems to happen for any other directives that creates a scope like ng-if as well. And it seems like, this is because the directive's isolated scope gets overwritten by the ng-repeat's child scope. And because of replace:true option ng-repeat becomes a part of the directive source element i.e <foo></foo> and the child scope of ng-repeat is calculated from the ultimate parent scope MainCtrl (Which appears to be wrong) and this causes the entire directive template to be bound to the child scope of controller and any interpolations are evaluated against that scope. Hence you see main controller's scope being expanded in the directive. This seems like a bug.

Related

Along with Transclude element, can i pass its scope too to a directive?

Moment i felt i have understood enough about Transclude i came across this statement :
Transclude allows us to pass in an entire template, including its scope, to a directive.
Doing so gives us the opportunity to pass in arbitrary content and arbitrary scope to a directive.
Does this mean, if there is a scope attached to Transclude element and it can be passed on to the directive ? If that's true then am not able to access that scope property inside directive template.
Let me take couple of steps back and explain with code about what am trying to do :
JSFiddle Link
My directive is directive-box and transclude: true is defined in Directive Definition Object(DDO).
Now there is a Child Div, which is the element to be Transcluded
<div ng-controller='TransCtrl'>Inside Transclude Scope : {{name}}</div>
and it has controller TransCtrl attached to it.
Now am trying to access $scope.name property which is part of TransCtrl from directive level after defining this in DDO :
scope: {
title: '#directiveTitle',
name: '='
}
Is this possible ?
This is more like a Parent scope trying to access Child scope property, is this permitted in JavaScript Protoypical inheritance ? Or is there something else i need to know ??
If this is not possible what does first statement mean ?
Transclude allows us to pass in an entire template, including its scope, to a directive.
UPDATE 1 :
My primary concern is Controller should remain with Transclude element, still we should be able to pass its (Transclude element) scope to Directive and then Directive should be able to consume that scope i.e., name from TransCtrl controller .
<div ng-controller='TransCtrl'>Inside Transclude Scope : {{name}}</div>
Above line of code should remain as is.
I may be completely wrong with my question but please let me if this can be accomplished.
The problem seems to be with the way the controller is defined within the ng-transcluded html.
I have made it clearer by using
the bindToController construct
using a controller at the directive level
Refer this fiddle for a working example.
controllerAs: "TransCtrl",
bindToController: true
And your statement, 'Parent scope trying to access Child scope property' is incorrect right? Since we are trying to use the parent scope property, i.e. name from within the child (ng-transcluded content), which is possible with protypical inheritance, and not the other way around.
Does this answer your question: https://jsfiddle.net/marssfa4/4/?
In it I have created a new controller on the outside (effectively replacing the functionality of your rootScope for inside the directive) and I made the directive's controller be set inside your controller template.
The long and short of it is though that you can see that it is possible to transclude html along with its scope even into a directive with its own scope.
The html:
<div ng-app='myApp' ng-controller="OutsideScope">
<h1>{{externalWorld}}</h1>
<div directive-box directive-title='{{directiveWorld}}' name='name'>
<div>Inside Transclude Scope : {{name}}</div>
</div>
</div>
JS (includes Update 1):
angular.module('myApp', [])
.directive('directiveBox', function() {
return {
restrict: 'EA',
scope: {
title: '#directiveTitle',
name: '='
},
transclude: true,
template: '<div ng-controller="TransCtrl">\
<h2 class="header">{{ title }}</h2>\
<div class="dirContent">Directive Element</div>\
<div>Outside Transclude Scope : {{name}}</div>\
<div class="content" ng-transclude></div>\
</div>'
}
})
.controller('TransCtrl', function($scope) {
$scope.name = 'Transclude World'
})
.controller('OutsideScope', function($scope) {
$scope.name = 'External World'
})
.run(function($rootScope) {
$rootScope.externalWorld = 'External World',
$rootScope.directiveWorld = 'Here comes directive'
});
UPDATE 1: JSFIDDLE
I restored the original scope declarations as the scope: false was a mistake.
If I understand your comment correctly you want to leave the controller on the element to be transcluded but still have the {{name}} within that element ignore its immediate controller and use as controller its parent (i.e. the directive's) scope.
The reason I placed the controller within the template directive is because that is the only way to limit the directive's scope on the directive and not its transcluded elements. If you are explicitly placing a controller on an element, then regardless of whether it is contained within a directive with another scope, its closest scope will override whatever scope has been declared on the directive. In other words, regardless of what the directive's scope is, the {{name}} in
<div ng-controller='TransCtrl'>Inside Transclude Scope : {{name}}</div>
will always be whatever $scope.name is in TransCtrl.

Nested - transcluded items - scope clarification?

I already know how transclusion works ( within first level only I guess) , bUt I have a question about nested transcluded item's scope.
Ok so I have this code :
<body ng-app="docsTabsExample" ng-controller="ctrl">
<my-tabs>
<my-pane title="Hello">
<h4>Hello , The value of "i" is => {{i}}</h4>
</my-pane>
</my-tabs>
</body>
Basically I have a controller , <my-tabs> and <my-pane >.
Looking at myTabs directive :
.directive('myTabs', function()
{
return {
restrict: 'E',
transclude: true,
scope:
{},
controller: ['$scope', function($scope)
{
$scope.i = 2;
}],
template: '<div ng-transclude></div>'
};
})
I know that the content of the directive will have access to the outer directive's scope
So the yellow part will have access to the outer scope ( which is the main controller scope) :
Here is the code for myPane directive :
.directive('myPane', function()
{
return {
require: '^myTabs',
restrict: 'E',
transclude: true,
scope:
{
},
controller: function($scope)
{
$scope.i = 4; //different value
},
template: '<div ng-transclude></div>'
};
})
The program starts with :
.controller('ctrl', function($scope)
{
$scope.i = 1000;
})
The output of the program is :
Hello , The value of "i" is => 1000
But
According to the documentation : myPane's transcluded data should have access to the outer scope of the directive which is myTabs directive which has the value i=2.
But myPane has an isolated scope so it does NOT inherit the scope from myTabs.
Question
So does it goes one level more higher to the controller's scope in order to get i=1000 ?? (Clarification , I'm not asking how can I make i get another value - I'm asking why/how it has the value of 1000).
I mean how does the hierarchy of scope looks here?
Is it like this?
controller's scope
|
+--------+---------+
| |
myTabs's mypanes's
transcluded transcluded
data's scope data's scope
the docs says :
The transclude option changes the way scopes are nested. It makes it
so that the contents of a transcluded directive have whatever scope is
outside the directive, rather than whatever scope is on the inside. In
doing so, it gives the contents access to the outside scope.
But what scope does the outside of myPAne directive has ?
In other words , why/how does i=1000?
FULL PLUNKER
EDIT FROM OP AFTER ANSWER
After installing and configuring PeriScope ( from #MarkRajcok) I can now see it visually :
From the docs on $compile
When you call a transclude function it returns a DOM fragment that is
pre-bound to a transclusion scope. This scope is special, in that it
is a child of the directive's scope (and so gets destroyed when the
directive's scope gets destroyed) but it inherits the properties of
the scope from which it was taken.
Parent Hierarchy (from $$childTail) is like:
-1 (root)
--2 (ctrl)
---3 mytab
----4 ($$transcluded = true)
------5 mypane
--------6 ($$transcluded = true)
Prototypical Hierarchy is like (screenshot from AngularJS Batarang)-
Updated plunker with scope id's printed in console should give you a better idea.
Why these are different, I am not very sure. Someone can throw light on this.
Why the value is 1000. Its because i needs to be provided as a bidirectional attribute = so the child scopes can modify it. I have updated the above plunker, you can see now the value responds to change in pane controller.
More on transcluded scopes -
Confused about Angularjs transcluded and isolate scopes & bindings
https://github.com/angular/angular.js/wiki/Understanding-Scopes

Accessing parent scope from isolated scope

I'm trying to write attribute directive that wraps element and decorates it with some other things along with additional directives. And I need to keep content inside the element intact. Something like this:
app.controller 'myCtrl', ($scope)->
$scope.name = "Gwendolyn"
app.directive 'niceName',($compile)->
restrict: 'A'
replace: yes
scope: niceName:"="
template: (element, attrs)->
"<div>
<div style='background:lightgray'>
#{element.html()}
</div>
<div>
<h1>{{nice}}</h1>
</div>
</div>"
controller: ($scope, $element)->
$scope.$watch 'hover',->
$scope.nice = if $scope.hover then $scope.niceName else ''
compile:(element, attrs)->
element.attr('ng-mouseover',"hover = true")
element.attr('ng-mouseout',"hover = false")
element.removeAttr('nice-name') # removing itself to avoid from falling into infinite loop
pre:(scope, iElement, iAttrs)->
$compile(iElement)(scope)
Markup:
<div nice-name="uglyname">
<h2>{{uglyname}}</h2>
<div>
Now that thing doesn't work at all, it won't render h2 part because now uglyname is unknown to the current scope. I can compile it with passing the parent scope, but then it totally breaks my controller. (see jsbin below)
So somehow I have to compile content of the element applying parent scope, add it to the template and then compile again applying local scope? Or I need to find a way to inherit properties of parent scope? Or I can't do it with attribute directive? Maybe I need to use element directive and transclusion?
Any ideas?
jsbin
i would do what #marfarma suggested as well as change you html to not include the brackets so:
scope: { niceName: "="},
and
nice-name="uglyname"

Access directive's controller from transcluded elements

I have the following scenario: A transclude directive with isolated scope which has a controller attached in it:
angular.module('app', [])
.directive('hello', function() {
return {
controller: function($scope) {
$scope.hello = "Hello World";
},
restrict: 'E',
replace: true,
transclude: true,
template: '<div class="hello" ng-transclude></div>',
scope: {}
};
});
I'm looking forward to access the directive's controller from the transcluded elements:
<hello>
<h1>Hello Directive</h1>
<p>{{ hello }}</p>
</hello>
However this doesn't seems to be possible. I tried accessing hello with $parent and $$nextSibling.
Is there any solution to this scenario? I'm not able to put the controller in wrapper around the directive instance.
I created a codepen to demonstrate this behaviour: http://codepen.io/jviotti/pen/ktpbE?editors=101
You need to pass the variable into the hello directive as a binding, and also include it as part of the isolate scope: http://codepen.io/anon/pen/yoCkp
More information here: Confused about Angularjs transcluded and isolate scopes & bindings
Edit:
In the original example, the template expected the {{ hello }} variable in the parent root scope, but it is actually in the hello directive scope. In fact, the empty isolate scope in the directive means that it will not get any scope variables from its parent. So what the above change does is route the (non-existent) hello variable from the root scope into the directive, and then sets the value.
To further illustrate this, you can take a look at this: http://codepen.io/anon/pen/pJEqjq - you will see that the controller is setting $rootScope.hello and it works as well. Though this is not advised because relying on the rootScope variables across different controllers can get messy.

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.

Resources