Why must I set "trasnclude: true" in an Angular DDO? - angularjs

I know what transclude: true does, but I've always wondered: "why must I put transclude: true in my DDO as well as a ng-transclude in my template?".
What's going on internally that forces angular to be redundant? Is it security/XSS protection? Performance?

The ngTransclude documentation explains the separation of the two (emphasis mine):
[ngTransclude is a] directive that marks the insertion point for the transcluded DOM of the nearest parent directive that uses transclusion.
This means:
transclude: true indicates that the directive makes its content available for transclusion.
ng-transclude indicates where the content should go.
A directive with transclusion enabled doesn't actually have to handle the transclusion of it's own content. It can let a child element choose where to put the transcluded content.
Here is a (trivial) example that shows how transclusion can be handled by a child directive inside a parent:
<!-- Application template -->
<parent-el>
<h1>Transcluded content</h1>
</parent-el>
<!-- <parent-el> template -->
<p>I am the parent element</p>
<child-el></child-el>
<!-- <child-el> template -->
<p>I am the child element</p>
<div ng-transclude></div>
This is how the content will then be rendered in the page:
<!-- Rendered content -->
<parent-el>
<p>I am the parent element</p>
<child-el>
<p>I am the child element</p>
<div>
<h1>Transcluded content</h1>
</div>
</child-el>
</parent-el>

There are three kinds of transclusion depending upon whether you want to transclude just the contents of the directive's element, the entire element or multiple parts of the element contents:
true - transclude the content (i.e. the child nodes) of the directive's element.
'element' - transclude the whole of the directive's element including any directives on this element that defined at a lower priority than this directive. When used, the template property is ignored.
{...} (an object hash): - map elements of the content onto transclusion "slots" in the template.
-- https://docs.angularjs.org/api/ng/service/$compile#transclusion

Related

Showing or hiding the parent element of a custom directive

I have a query concerning a custom directive example found within Angular's docs here.
When the select method is called (from the parent controller in order to show / hide a pane), the relevant <div class="tab-pane" ng-show="selected"> element is shown / hidden, which is how it should function as per the example, naturally.
However, I'd like to hide the parent element, which is <my-pane title="..."> so that <my-pane title="..."> isn't left visible (even though all it's child content is hidden.) In other words, move the ng-show directive from <div class="tab-pane" ng-show="selected"> to <my-pane title="..." ng-show="selected">
I assumed that each <my-pane title="..."> had its own isolated scope with a unique $scope.id, so it should be easy to accomplish by targetting the relevant scope and updating a selected value via the tab links, but for the life of me I cannot seem to make it work.
After inspecting both <my-pane title="...">'s in console it appears as they both have the same $scope.id, which shouldn't be the case as they're separate scopes, right?
Something is missing from my understanding of isolated scopes perhaps. Any pointers would be great.

Issue with isolated scope and "replace: true" in angular directive

I have been struggling with a scoping issue when making an error message directive using AngularJS.
I have an ng-if and ng-class directive as part of the directive template, but the expression in the ng-class directive always seemed to return a blank string, unless:
I removed the ng-if directive.
or, I removed the 'replace' key in the directive definition object.
Looking at the compiled output for my directive, it looks like an isolated scope is being created if the ng-if or the replace key is removed, but if they are both left in, then there are no ng-isolate-scope classes in the html output, just ng-scope.
I would really like to understand exactly what is going on here and would be grateful for any explanations.
Directive Definition
angular.module('myMessages')
.directive('pageMessages', function() {
return {
restrict: 'E',
replace: true,
scope: {
messages: '='
},
controller: function($scope) {
$scope.severity = 'alert-success';
},
template: '<div ng-if="messages.length > 0">' +
'<div class="alert" ng-class="severity">' +
'<ul>' +
'<li ng-repeat="m in messages">{{::m.message}}</li>' +
'</ul>' +
'</div>' +
'</div>'
};
});
Output (note no alert-danger class is added)
<!-- ngIf: messages.length > 0 -->
<div ng-if="messages.length > 0" messages="messages" class="ng-scope">
<div class="alert" ng-class="severity">
<ul>
<!-- ngRepeat: m in messages -->
<li ng-repeat="m in messages" class="ng-binding ng-scope">test error</li>
<!-- end ngRepeat: m in messages --></ul>
</div>
</div>
<!-- end ngIf: messages.length > 0 --></div>
alert-danger class is added after removing replace (removing ng-if would work as well)
<page-messages messages="messages" class="ng-isolate-scope">
<!-- ngIf: messages.length > 0 -->
<div ng-if="messages.length > 0" class="ng-scope">
<div class="alert alert-danger" ng-class="severity">
<ul>
<!-- ngRepeat: m in messages -->
<li ng-repeat="m in messages" class="ng-binding ng-scope">test error</li>
<!-- end ngRepeat: m in messages -->
</ul>
</div>
</div>
<!-- end ngIf: messages.length > 0 -->
</page-messages>
The job of truthy ng-if comes to cloning an original element and giving it inherited scope. It uses transclusion for that, this allows ng-if to get inherited scope on an element with isolated scope, avoiding $compile:multidir error with Multiple directives requesting new/isolated scope verdict.
The good thing is that it won't throw an error if it is used on an element with isolated scope. The bad thing is when used on a directive with higher priority (ng-if priority is 600) it will just replace it, ignoring its scope. And another bad thing is that when used on on root template element of a directive with isolated scope (like this one) it will just replace an element with cloned one that inherits its scope from parent scope (belonging to directive's parent element, because its own element was already replaced with replace).
So it just gets severity value from pageMessages parent scope and evaluates ng-class expression to empty string if it doesn't exist.
The solution is to not use ng-if on root element of a directive with replace flag. replace flag has got deprecation status, which means that issues won't be fixed. When directive's template gets an extra <div> wrapper (though it may serve against the purpose of replace), everything should work as intended.
By using replace=true and ng-if and isolate scope together, the code is attempting to directives with different scopes on the same element.
From the Docs:
In general it's possible to apply more than one directive to one element, but there might be limitations depending on the type of scope required by the directives. The following points will help explain these limitations. For simplicity only two directives are taken into account, but it is also applicable for several directives:
no scope + no scope => Two directives which don't require their own scope will use their parent's scope
child scope + no scope => Both directives will share one single child scope
child scope + child scope => Both directives will share one single child scope
isolated scope + no scope => The isolated directive will use it's own created isolated scope. The other directive will use its parent's scope
isolated scope + child scope => Won't work! Only one scope can be related to one element. Therefore these directives cannot be applied to the same element.
isolated scope + isolated scope => Won't work! Only one scope can be related to one element. Therefore these directives cannot be applied to the same element.
-- AngularJS Comprehensive Directive API - scope
replace:true is Deprecated1
From the Docs:
replace ([DEPRECATED!], will be removed in next major release - i.e. v2.0)
specify what the template should replace. Defaults to false.
true - the template will replace the directive's element.
false - the template will replace the contents of the directive's element.
-- AngularJS Comprehensive Directive API
From GitHub:
Caitp-- It's deprecated because there are known, very silly problems with replace: true, a number of which can't really be fixed in a reasonable fashion. If you're careful and avoid these problems, then more power to you, but for the benefit of new users, it's easier to just tell them "this will give you a headache, don't do it".
-- AngularJS Issue #7636

Angular directive new scope rule

The Angular directive documentation says: "If set to true, then a new scope will be created for this directive. If multiple directives on the same element request a new scope, only one new scope is created. The new scope rule does not apply for the root of the template since the root of the template always gets a new scope."
My question is the last sentence. I assume that "template" refers to the directive's template, but on testing a simple directive whether it has or doesn't have a template, no new scope is created without setting "scope: true". Am I missing something here?
Perhaps the "root of the template" there means the root element which matches ngController directive. In the example below, the first and second myCustomer directives belong to the same "root template", but the third one is different.
<div ng-controller="Controller">
<my-customer></my-customer>
<div>
<my-customer></my-customer>
</div>
</div>
<div ng-controller="Controller">
<my-customer></my-customer>
</div>

Can't put ng-controller on directive?

I have a very simple angular example at http://jsfiddle.net/7eL47/3/. The rendered output of the code shows "Foobar" on the page.
The template for this rendered output is:
<div ng-app="myApp" ng-controller="MenuController">
<unordered-list>
Foo{{foo}}
</unordered-list>
</div>
However, when I change the location of ng-controller to the unordered-list as shown below, "Foobar" no longer appears--it's just "Foo." The value of {{foo}} is never replaced with "bar".
<div ng-app="myApp">
<unordered-list ng-controller="MenuController">
Foo{{foo}}
</unordered-list>
</div>
Why don't I see "Foobar" still when I change the ng-controller directive to be on the unordered-list element?
In your first example, ngController is a parent to unordered-list. So it has visibility to foo.
Your second example:
<unordered-list ng-controller="MenuController">
Results in two sibling scopes each with a parent of ngApp.
Both your directive and the ngController directive use scope: true. scope: true causes a child scope to be created for that directive which inherits from the parent. Therefore you end up with sibling scopes.
Thus, in the second example, $scope.foo is not visible, since it's no longer on the scope unoderedList inherits from (but rather on a sibling scope).

AngularJS : How to properly transclude child elements in custom directive?

Here's some code : link
I'm trying to create a directive that wraps its children in some boilerplate. But if the children have ng-if controlling their appearance, the "transclusion" doesn't work. Well it sort of does, but as you can see the ng-if logic is not getting passed through correctly.
I'd like to know how to fix it, but also where (if anywhere) this is described in the Angular docs.
The problem is that Angular initially replaces ngIf with a comment that it uses to track where to place the conditional code. It's easiest to see with an example.
Your HTML:
<div control-group>
<label>Test</label>
<input type="text" ng-model="editing.name" />
<span class="text-error" ng-if="editing.name.length != 3"> Name must be 3 characters </span>
</div>
Looks like this inside your transclude function's cloned variable (transclude(function (cloned) {):
<div control-group>
<label>Test</label>
<input type="text" ng-model="editing.name" class="ng-valid ng-dirty">
<!-- ngIf: editing.name.length != 3 -->
</div>
So, the element with class text-error that you're filtering on (below) isn't in cloned. Just the comment is there.
var inputsAndMessages = cloned.filter('input, button, select, .text-error');
Since you're only transcluding elements that match the above filter the ngIf comment is lost.
The solution is to filter for comments as well and add them in your append (so Angular maintains it's reference point to the ngIf). One way to do that is to replace the above with this (using the fact that an html comment is node type 8)
var messages = cloned.filter(function(){ return this.nodeType == 8; }); //comments
var inputs = cloned.filter('input, button, select')
var inputsAndMessages = inputs.add(messages);
Working plunker
You need to tell the directive where to place child elements with the ng-transclude directive: (plnkr)
template: "<div class='control-group' ng-transclude>" +
"<label class='control-label' for=''></label>" +
"<div class='controls'></div>" +
"</div>",
From the docs:
Directive that marks the insertion point for the transcluded DOM of the nearest parent directive that uses transclusion.
Any existing content of the element that this directive is placed on will be removed before the transcluded content is inserted.
I wasn't sure what exactly your intent was since you have the input and label both in the template and as children in the HTML. You might want to place your ng-transclude elsewhere.

Resources