Why use terminal: true instead of removing lower priority directives? - angularjs

Related: How to understand the `terminal` of directive?
Why would someone set terminal: true and a priority on a directive rather than simply removing the lower priority directives? For example, they could write:
<tag directive-1 directive-2 directive-3></tag>
... and they could add priority: 100 and terminal: true to directive-3, so that only directive-3 would be applied to the element.
Why wouldn't someone instead change their template to:
<tag directive-3></tag>
Perhaps it simplifies the code in some cases by allowing multiple directives to be added to an element and offloading the work of deciding which ones to actually apply to Angular?
Thanks.

Setting the priority and terminal options is not about erasing directives, it's declaring the order of compilation and linking. Everybody points to ng-repeat as the prime example of priority + terminal + transclude, so I'll give a extremely simplified version of ng-repeat:
app.directive('fakeRepeat', function($log) {
return {
priority: 1000,
terminal: true,
transclude: 'element',
compile: function(el, attr, linker) {
return function(scope, $element, $attr) {
angular.forEach(scope.$eval($attr.fakeRepeat).reverse(), function(x) {
var child = scope.$new();
child[attr.binding] = x;
linker(child, function(clone) {
$element.after(clone);
})
})
}
}
}
});
The fake repeat directive can be used as so:
<ul>
<li fake-repeat="things" binding="t" add-letter>{{ t }}</li>
<ul>
Now extra directives can be attached to the same li that contains fake repeat, but their priority + terminal options will determine who gets compiled first, and when linking happens. Normally we expect the li element to be cloned and for and for the add-letter directive to be copied for each binding t, but that will only happen if add-letter
has a lower priority than fake-repeat.
Case 1: add-letter priority is 0
Linking is executed for each li generated.
Case 2: add-letter priority is 1001
Linking is executed before fake-repeat and thus before the transclude happens.
Case 3: add-letter priority is 1001 and terminal is true
Compilation stops before fake-repeat so the directive is never executed.
Here is a plunker with console logging for further exploration.

I believe terminal was created to work with directives that use transclusion or directives that are meant to replace all of an element's content.
If an element uses terminal then it does not want applicable directives to compile during the initial directive collection. The initial collection is triggered either by Angular's bootstrap process or a manual $compile. Just because the terminal directive does not want the lower priority directives to compile, does not mean that it does not want the directives to run later, which is why transclude is a perfect use case.
The contents are compiled and stored as a link function that can be evaluated against any scope at any time. This is how ngRepeat, and ngIf perform.
When writing a directive that uses transclusion maybe consider if it should use terminal as well.
I don't believe it's very useful when using it with directives that don't use transclude.

Related

AngularJS: Remove the directive of lower priority?

I'm writing a directive which can remove ng-disabled programmically (e.g.: when current user has no permission for editing, remove ng-disabled and add a static disabled attribute directly.)
I read the official doc about $compile, it says I can specify priority option to decide the compilation order. The source code of latest AngularJS 1.7.9 shows that the priority of ngDisabled is 100, so I wrote this :
app.directive('removeNgDisabled', function ($compile) {
return {
priority: 101, // The priority of ngDisabled is 100
compile: function ($el, $attrs) {
const el = $el[0]
el.removeAttribute('ng-disabled') // ng-disabled should be removed before it is compiled... isn't it?!
}
}
})
<input ng-model="foo" ng-disabled="true" remove-ng-disabled=""/>
I don't know where do I get wrong, ng-disabled disappeared from DOM indeed, but ng-disabled still works...
It seems impossible to remove the directives have already existed on an HTML element as soon as Angular start to $compile that element.
The official document only says that:
The priority is used to sort the directives before their compile functions get called.
but it seems not mean to you can do any thing before the sorting. That is to say, when the "higher-ordering" directive has been compiled, the lower-ordering directives had already been push in the execution queue (I guess, I didn't dig into the internal implementation.) and had become unable to be removed.
I didn't dig into the internal implementation of the mechanism of directive in AngularJS; but at least, in the official document, there's no way to "cancel" or "remove" existed directives. So the only way to achieve similar behavior is to implement a homebrewed ng-disabled by yourself.

terminal: true and priority > 100 causes "#" parameter to stop working

The code below "breaks" interpolated scope value p1: "#". Am I doing something wrong or is this an Angular bug?
.directive("foo", function($compile){
return {
terminal: true,
priority: 200, // anything > 100
templateUrl: "foo.html",
scope: {
p1: "#", // doesn't work
p2: "=", // works
p3: "&" // works
},
link: function(scope, element){
// this also doesn't help
$compile(element.contents())(scope);
}
};
});
The template foo.html is:
<div>{{p1}}</div>
<div>{{p2}}</div>
The output for <foo p1="{{name}}" p2="name" ng-init="name = 'something'"></foo> is:
{{name}}
something
plunker
The problem is with the priority that you have set on the directive.
Normally, The priority is only relevant when you have multiple directives on one element. The priority determines in what order those directives will be applied/started. In most cases you wouldn't need a priority, but sometimes when you use the compile function, you want to make sure that your compile function runs first.
Now the problem in your case is that you have set the priority of your
directive as 200. Where as the priority of the {{}} interpolation is
100.
Hence in your case, even before the values are interpolated, the directive compiles and hence your p1 attribute has a value of {{something}}.
Keep the priority of your directive anything less than 100 and things
will work out as expected.

Executing code after AngularJS template is rendered

I have a table that renders rows with ng-repeat. Inside one of the cells there is a select that is rendered with ng-options.
<tr ng-repeat="item in data.items" repeat-done>
<td >
...
<select class="selectpicker"
ng-model="person" ng-options="person.Surname for person in data.Persons track by person.Id">
<option value="">Introduce yourself...</option>
</select>
...
<td>
</tr>
When repeat is done I need to turn select into bootstrap-select (a nice looking dropdownlist). So after a little bit of researching I added the following directive:
app.directive('repeatDone', function () {
return function (scope, element, attrs) {
if (scope.$last) {
setTimeout(function() { $('.selectpicker').selectpicker(); }, 1);
};
};
});
which is specified at tr (see above).
It works. But I am a little bit worried whether there is a chance it will not work on slow PC/tablet/etc. As I understand AngularJS has an asynchronous nature. So while the last element of ng-repeat is being processed, it is still possible ng-options for this element (or may be for some previous element too) is not rendered. Or am I paranoiac?
You shouldn´t synchronize your directives using timeout. A lot of problems can appear.
You can use the option priority to sort when your directives are going to be used. Directives are executed on descendant order.
ngRepeat has priority 1000 (https://docs.angularjs.org/api/ng/directive/ngRepeat)
select has priority 0 (https://docs.angularjs.org/api/ng/directive/select)
If you declare your directive with a negative priority it will execute after ng-options.
app.directive('repeatDone', function () {
return {
priority: -5,
link: function (scope, element, attrs) {
$('.selectpicker').selectpicker();
}
}
});
According to Angular documentation:
priority
When there are multiple directives defined on a single DOM element,
sometimes it is necessary to specify the order in which the directives
are applied. The priority is used to sort the directives before their
compile functions get called. Priority is defined as a number.
Directives with greater numerical priority are compiled first.
Pre-link functions are also run in priority order, but post-link
functions are run in reverse order. The order of directives with the
same priority is undefined. The default priority is 0.
https://docs.angularjs.org/api/ng/service/$compile

The way angular bootstraps

I have a question about the way Angular bootstraps. Consider this directive:
app.directive('finder', function () {
return {
restrict: 'E',
replace: true,
template:'<div><input type="text" data-ng-model="my-input" style="width:80%;" /></div>',
compile: function (element, attributes) {
if (attributes.hasOwnProperty('required')) {
element.removeAttr('required');
element.find(':input').attr('required', true);
element.removeClass("ng-invalid ng-invalid-required");
}
return {
post: function postLink(scope, iElement, attributes) {
// Do some other stuff
}
}
}
}
})
And I use this directive as follows:
<div>
<finder required></finder>
</div>
Some CSS:
.ng-invalid-required {
background: yellow;
}
As you can see in the directive, in the compile phase, I remove the required attribute from the element and add the attribute to the input element. The result is what you (sort of) should expect: the main element doesn't have the attribute anymore, but the input field does.
Now the 'strange' part: The output shows that the div also has a yellow background. As it turns out, the main element (div) has the ng-invalid-required class! This is strange because I removed the required attribute in the compile phase...
How is this possible?
Apparently, Angular scans the whole DOM for directives and collect the directives with the belonging elements. After the scan, Angular is going to compile all the directives. At one point it is going to compile my finder directive. This directive removes the required attribute and add it to my input field. Later on it compiles the required attribute. Because the 'original element' is in the collected list it is going to add the ng-invalid-required class to the linked element (the div).
Is this correct? Or is there another explanation for this behaviour?
EDIT (thanks to Nikos Paraskevopoulos suggestion in his comment).
Interesting: When I add this to the directive:
priority: 1000,
terminal: true
It works like I would expect. But when I leave one of those properties, it doesn't work anymore. Could someone explain to me why this works?
(Moved from comment to answer + explanation)
Try specifying both priority (to a value so that your directive runs before the requiredDirective) and terminal: true in your directive definition.
Explanation:
The reason for the original problem is that the requiredDirective executes on the element. Obviously, as you point out, Angular scans the whole DOM for directives and collects the directives and executes them. To clarify: first it collects them, then executes them.
This means that the requiredDirective will execute despite your compile() function having removed the required attribute, because Angular has already "collected" the directives.
The terminal: true property:
If set to true then the current priority will be the last set of directives which will execute
This means that if you set priority to a value so that your directive executes before the requiredDirective, the latter will not execute at all! Obviously, Angular stops the collection of directives when it encounters one with terminal: true.
"required" is compiled before your element, so I think that even if you remove required attribute it was already marked as required and angularjs adds ng-invalid class.
Try to change the priority of your directive to 1000, so it will be compiled before required directive and so you can remove required attribute:
app.directive('finder', function () {
return {
priority: 100,
restrict: 'E',
...
};
});

AngularJS: is there a difference between $transclude local in directive controller and transclude parameter in link function?

I implemented a directive that transcludes multiple fragments of child content into a template. It works but seems simpler than most of the examples I have seen, and raised a few questions about how transclusion works.
Here is the directive:
module.directive('myTransclude', function() {
return {
restrict: 'A',
transclude: true,
replace: true,
scope: true,
template: '<div style="border: 1px solid {{color}}"><div class="my-transclude-title"></div><div class="my-transclude-body"></div></div>',
link: function(scope, element, attrs, controller, transclude) {
// just to check transcluded scope
scope.color = "red";
transclude(scope, function(clone, scope) {
Array.prototype.forEach.call(clone, function(node) {
if (! node.tagName) {
return;
}
// look for a placeholder node in the template that matches the tag in the multi-transcluded content
var placeholder = element[0].querySelector('.' + node.tagName.toLowerCase());
if (! placeholder) {
return;
}
// insert the transcluded content
placeholder.appendChild(node);
});
});
}
}
});
and here is example usage:
<div ng-controller="AppController">
<div my-transclude>
<my-transclude-title> <strong ng-bind="title"></strong>
</my-transclude-title>
<my-transclude-body>My name is {{name}} and I've been transcluded!</my-transclude-body>
</div>
</div>
You can see it in action in this fiddle.
Please notice a few things:
It matches fragments to template placeholders by element class, rather than explicitly defined child directives. Is there any reason to do this one way or another?
Unlike many examples I've seen, it doesn't explicitly use the $compile service on the child fragments. It seems like Angular is compiling the fragments after transclusion, at least in this simple case. Is this always correct?
It uses the (barely documented) transclude argument to the link function, rather than the other (barely documented) approach of injecting the $transclude local into the controller directive. After hearing so many admonitions not to manipulate DOM in controllers, the controller approach seems like an odd construct and it feels more natural to handle this in the link function. However, I tried it that way and it seems to work the same. Is there any difference between the two?
Thanks.
EDIT: to partially answer question #2, I discovered that you do need to explicitly compile transcluded content that is not cloned from the template where the directive was applied. See the difference in behavior here: http://jsfiddle.net/4tSqr/3/
To answer your question in regards to the differences between $transclude function in directive controller vs linking function, first we need understand that $transclude function can be accessed through directive compile, controller and linking functions.
UPDATE: According to 1.4 Angular documentation, compile(transclude) has been deprecated! So transclude function can only be accessible in your directive Controller or Linking function. (See official documentation for detail explanation)
There is a big difference when used $transclude in compile phase vs. $transclude in controller and linking phase due to during compiling phase, you don't have access to $scope vs. using in controller and linking functions where $scope (controller) and scope (linking) are accessible. With that said, the only difference in using $transclude in directive controller vs. linking is order of execution. For multiple nested directives, its relatively safe to use $transclude during your linking phase instead of using it in your controller.
The order goes like this:
parentDirectiveCompile -> childDirectiveCompile (Directive Compile)
parentDirectiveControllerPre, parentDirectiveControllerPost -> childDirectiveControllerPre, childDirectiveControllerPost (Directive Controller)
childLinkFunction -> parentLinkFunction
Notice how childLinkFunction gets executed first before parentLinkFunction? (Order of execution)
Helpful Resource:
Angular directives - when and how to use compile, controller, pre-link and post-link
Hopefully this answer can be helpful to you!
after some investigation:
After the release of Angular 1.20 pre-existing child nodes of a compiled directive that has a isolate scope will no longer inherit the new isolate scope because they have already been assigned to the parent scope. So... the built in transclude method that uses the attribute ng-transclude will only transclude templates to the desired location in this case but will not transclude pre-existing html to this location. This means that if you had a directive with an isolate scope and you wanted the html that was already there to be compiled to the new isolate scope you would need to use the transclude linker function inside of the directive.
You can see a working case of this problem here ui-codemirror placed within custom directives fails without an error

Resources