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.
Related
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',
...
};
});
Is there a way to interpret the content of a div having my directive as an attribute :
example :
<div my-directive>{{1+1}}</div>
and my link function looks like this :
link = (scope, element, attrs) =>
interpretedString = element.text()
in this example interpretedString is equal to {{1+1}} instead of 2
any help? the scope.$eval evaluates attributes, but what if I want to evaluate the text of a directive?
thanks
In order to interpret the string, you need to interpolate it thanks to the angular's $interpolate service.
Example:
link = (scope, element, attrs) =>
interpretedString = $interpolate(element.text())(scope)
Just to back up a bit, the contents of an element could be anything -- an array of element trees, text nodes, comments, and (unless you declare your directive "terminal") it all gets evaluated recursively and could have directives inside directive. I think you'd be much better off passing an interpolated string as an attribute. I mean you could do it this way, but whoever's using your widget wouldn't really expect that.
Something like:
<div my-directive my-attr="{{1+1}}"></div>
Or even (if that attribute is "primary"):
<div my-directive="{{1+1}}"></div>
From there, instead of $interpolate(element.text())(scope) you'd have $interpolate(attrs.myDirective)(scope)
Of course, the nature of Angular is everything is dynamic and can update all the time. What if the expression didn't just have constants, as a real expression likely wouldn't. If it were:
{{1+foo}}
Then the question is do you care about it changing? If not, $interpolate is fine. It captures the initial value. If you do care about it updating, you should use $observe.
attrs.$observe('myDirective', function(val) {
// if say "foo" is 5, val would be 6 here
});
Then later if you're like scope.foo = 8 the callback runs and passes 9.
Alternative is to do
<my-directive ng-bind="1+1" />
var two = $scope.$eval('ngBind');
:-p
.directive('myDirective', function () {
return {
restrict: 'E',
scope: {
ngBind : '='
},
link: function (scope, element, attrs) {
console.log('expecting string "1+1": '+attrs.ngBind);
console.log('expecting evaluated string "2": '+scope.$eval('ngBind'));
}
};
});
side note: <div ng-bind="val"></div> is almost the same as <div>{{val}}</div>. Except that u save 4 brackets and never see them flashing into view when things might go slower whatsoever reason.
Angular Documentation says: -
The compilation of the DOM is performed by the call to the $compile()
method. The method traverses the DOM and matches the directives. If a
match is found it is added to the list of directives associated with
the given DOM element. Once all directives for a given DOM element
have been identified they are sorted by priority and their
compile() functions are executed.
The ng-repeat directive I believe has a lower priority than custom directives, in certain use cases like dynamic id and custom directive. Does angular permit tinkering with priority of directives to choose execution of one before the other?
Yes, you can set the priority of a directive. ng-repeat has a priority of 1000, which is actually higher than custom directives (default priority is 0). You can use this number as a guide for how to set your own priority on your directives in relation to it.
angular.module('x').directive('customPriority', function() {
return {
priority: 1001,
restrict: 'E',
compile: function () {
return function () {...}
}
}
})
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. The order of directives with the same priority is undefined. The default priority is 0.
AngularJS finds all directives associated with an element and processes it. This option tells angular to sort directives by priority so a directive having higher priority will be compiled or linked before others. The reason for having this option is that we can perform conditional check on the output of the previous directive compiled.
In the followed example, first add button and only after add class to current button:
Demo Fiddle
App.directive('btn', function() {
return {
restrict: 'A',
priority: 1,
link: function(scope, element, attrs) {
element.addClass('btn');
}
};
});
App.directive('primary', function() {
return {
restrict: 'A',
priority: 0,
link: function(scope, element, attrs) {
if (element.hasClass('btn')) {
element.addClass('btn-primary');
}
}
};
});
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.
I have a very strange phenomenon with a directive and an isolated scope, where the attributes in the scope work or do not work depending on the naming of the attribute. If I use
{check:'#check'}
it works just fine and as expected. However,if I use:
{checkN:'#checkN'}
the defined function never gets assigned. An example would look like:
HTML:
<item ng-repeat="list_item in model.list" model="list_item" checkN="checkName()" check="checkName()" position="$index"></item>'
Javascript
app.directive('item', function(){
return {
restrict: 'E',
replace : false,
scope:{
$index: '=position',
check: '&check',
checkN: '&checkN',
model:'='
},
template: '',
link: function(scope, element, attrs){
console.log(scope.check())
console.log(scope.checkN())
}
}
});
The console will then give me the following:
The checkName function has been called [which is the return string of the function]
undefined
It is really possible that it depends on the usage of capital letters? This would be very "unexpected" behaviour.
Thanks for your help
schacki
Html is case insensitive, therefore myAttribute and myattribute would be indistinguishable from each other depending on the browser. Angularjs' authors made a design decision about passing from html to javascript and vice-versa in terms of directives.
ngRepeat directive would be used as ng-repeat in the view(html).
Likewise, your directive checkN should be used as check-n for angular to recognise that as directive.