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
Related
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.
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.
I created my own directive in angularjs and I noticed that directive scope is not refreshing when I change scope in main controller.
I made simple example which after 3s changing scope value, but the value is not changed in the directive. The issue exists only if I assign directive provided value to the directive scope.
Entire example is available here: jsfiddle
My directive:
myApp.directive('textPresenter', function() {
return {
transclude : false,
restrict: 'E',
scope: {
adfName : '='
},
template: '{{xxx}}' ,
controller: function($scope){
$scope.xxx = $scope.adfName; //this is the issue
}
};
});
$scope.xxx should refresh, after scope change in main controller.
The scope value adfName in the template is bound to the one in your main controller by Angular, and will change when that one changes. If you change the template to:
template: '{{adfName}}'
as in http://jsfiddle.net/6bp7c/1/ , then you can see this.
However, from your comment, you say you can't do this. Then one possibility would be setup a watcher in your directive, to watch adfName, and set xxx to be equal to it when it changes:
$scope.$watch('adfName', function(newAdfName) {
$scope.xxx = newAdfName;
});
as can be seen at http://jsfiddle.net/7V9FV/ . This is necessary because
$scope.xxx = $scope.adfName
just copies the contents of adfName and put it into xxx. If adfName later changes, then the contents of xxx are unaffected.
I am curious as to why you have to copy the variable though, and not just change the template to use the one passed into the directive.
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.
I am writing my angularjs directive with definition like:
return {
restrict: 'EA',
link: link,
scope: true,
transclude: true,
replace: true,
controller: controller,
template: '<div class="wizard">' +
'<div ng-transclude></div>' +
'</div>'
};
I notice two scopes was created:
< Scope (003) --- parent scope of directive
< Scope (004) --- controller scope of directive which I think is child scope created by 'scope=true'. all my functions, properites defined in controller show up in this scope
< Scope (005) --- transclude scope which is empty
from the document I am expecting only one child scope was created because 'scope=true' will not create an isolated scope. this leads all elements replaced by 'ng-transclude' actually inherit Scope(005) and have no access to my functions/properties defined in controller because they are in Scope(004) which is a sibling of Scope(005).
I don't know what's going wrong, can somebody throw some lights here?
And when using Chrome debugger to watch my elements, I notice these elements were added by a class "ng-scope", however, how can I match "ng-scope" to scopes showing in batarang console? like show ng-scope's id.
thanks
scope: true will create a new child scope that prototypically inherits from the controller scope – this is Scope 004.
scope: { ... } would create a new child scope that does not prototypically inherit from the controller scope.
Either way, a new child scope is created.
In addition, because you are using transclude: true, another (transcluded) child scope 005 is created. Transcluded scopes always prototypically inherit from the controller scope.
As you already discovered, properties and functions that you define on the directive scope (i.e., inside your directive) are not available to the view because the view uses the transcluded scope.
The picture above is based on the following code:
app.directive('myDirective', function() {
return {
restrict: 'EA',
//link: link,
scope: true,
transclude: true,
replace: true,
controller: function($scope) {
$scope.dirProp1 = "dirProp1";
$scope.dirFunc = function() {}
},
template: '<div class="wizard">' +
'<div ng-transclude></div>' +
'</div>'
};
});
function MyCtrl($scope) {
$scope.parentCtrlProp1 = 'ParentCtrlProp1';
}
So, as you can see from the diagram, the transcluded scope (hence the transcluded content) can only access properties and functions defined on the controller scope (003), via the prototype chain.
how can I match "ng-scope" to scopes showing in batarang console? like show ng-scope's
id.
I'm not aware of any way to do this (which is why I wrote a tool to draw my own pictures).
it's hard to figure out without any jsfiddle for the context. What is your link function? and what is your controller function?
By the way, it is the normal behaviour as documentation says transclude:true creates also a new scope.
Look at here : https://github.com/angular/angular.js/wiki/Understanding-Scopes
The transcluded and the isolated scope (if any) are siblings -- the
$parent property of each scope references the same parent scope. When
a transcluded and an isolate scope both exist, isolate scope property
$$nextSibling will reference the transcluded scope.
So if you want to access the 2 siblings from one another, you have to use a 2-way binding with their parent