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',
...
};
});
Related
I'm using the fcsaNumber directive to check for the validity of numbers entered into an input box. I got this working, but the value im checking for is dynamic, meaning it can change depending upon what options have been selected. It appears this directive only initializes once. what do I need to do to get it to reinitialize when the options have changed?
//this works fine
<input name="Employees" type="text" ng-model="Input.Employees" fcsa-number="{min: 1}" required="">
//this only inits once, so its not dynamic
<input name="Employees" type="text" ng-model="Input.Employees" fcsa-number="{{getMin()}}" required="">
Rebuilding the whole directive would work, but it's a brute-force solution -- and particularly undesirable for directives like this one that accept user input, which would get blown away were you to re-init the directive.
Instead you can use scope.$watch or attrs.$observe to allow the directive to more readily respond to changes in its attribute values:
module.directive('foo', function() {
return {
scope: {
'bar': '#'
},
link: function(scope, element, attrs) {
scope.$watch('bar', function() {
// runs when scoped value 'bar' changes
});
attrs.$observe('baz', function() {
// Similar to $watch, but without isolate scope
});
}
}
});
AngularJS : Difference between the $observe and $watch methods has a good explanation of the differences between $observe and $watch.
You could potentially use ng-if to initialize / tear down the directive in question. (ng-if docs)
The ngIf directive removes or recreates a portion of the DOM tree
based on an expression. If the expression assigned to ngIf evaluates
to a false value then the element is removed from the DOM, otherwise a
clone of the element is reinserted into the DOM - CodeHater
Here's a great stack overflow answer on the differences between ng-if and ng-show.
I'm using Jasmine+Karma and need to find a way to test an angular directive used to alert the user if passwords don't match - it seems to accomplish this with a directive the renders true or false, and there is with ngShow on the HTML that displays when this, along with a couple other properties, are true.
Here's the directive. I'm having a little difficulty understanding how it works.
app.directive('passwordMatch', [function () {
return {
restrict: 'A',
scope:true,
require: 'ngModel',
link: function (scope, elem, attrs, control) {
var checker = function () {
var e1 = scope.$eval(attrs.ngModel);
var e2 = scope.$eval(attrs.passwordMatch);
if(e2!=null)
return e1 == e2;
};
scope.$watch(checker, function (n) {
control.$setValidity("passwordNoMatch", n);
});
}
};
}]);
<small class="errorMessage" data-ng-show="signupForm.password2.$dirty && signupForm.password2.$error.passwordNoMatch && !signupForm.password2.$error.required"> Password do not match.</small>
So as far as I'm able to tell, what's happening is scope.$watch is watching the checker function for a change, which then gets put into the listeners argument and updates the property on the DOM?
How does it do that, then when the purpose is to detect if passwords do not match - if they don't match, then e1 === e2 is false, and this value is passed into $scope.watch(checker, function(n)...? If that was how it worked, then wouldn't it set the of value passwordNoMatch to false, which would make ng-show hidden?
Or is that not how it works, it works another way?
And, before that, what's going on with the link: function part?
Where is the scope coming from (it just says scope:true in the directive?)
And the elem? And the attr (the attributes from html elements?)?
Is angular just looking through a list of each and every one of them, the elements and attributes, and the scope? Is there a passwordMatch property already in there somehow?
What is $eval doing?
You have a lot of questions here and spending some time with the Angular docs would help you answer a lot of them. I'll put links to the relevant docs so you can get a fuller explanation.
So as far as I'm able to tell, what's happening is scope.$watch is watching the checker function for a change, which then gets put into the listeners argument and updates the property on the DOM? How does it do that?
I think you are almost there. When you watch a function it gets called on every digest cycle and if the return value of the function has changed then it calls the function you passed as the second argument i.e.
function(n){
control.$setValidity("passwordNoMatch", n);
}
To understand this function you need to understand what the require option does. You have set require to 'ngModel' and basically what this means is that the control variable passed in as the 4th argument to your link function is a reference to NgModelController, which provides an API for the ngModel directive.
The $setValidity("passwordNoMatch", n) bit sets the value of the property 'passwordNoMatch' on the $error object to the value of n. So, n here is the return value of the function you are watching and the $error object is a property of the FormController that is available on all angular forms which have the name attribute defined in the HTML form tag. So, basically this function is what sets the value of the signupForm.password2.$error.passwordNoMatch that you see in your <small> tag.
Where is the scope coming from (it just says scope:true in the directive?)
The scope in link function is (from the Angular docs)..
The scope to be used by the directive for registering watches.
The scope: true bit is what tells Angular whether to create a new scope for the directive or to create an 'isolate scope' which does not 'prototypically inherit from the parent scope'. I would recommend that you spend some quality time reading about the directive definition object if you really want to grok directives.
What is $eval doing?
The first argument passed to scope.$eval is executed as an Angular Expression and the result is returned. So in your code I suspect they will both return strings from your password fields which you then check to see if they match.
Hope that helps.
My directive "addOnce" is required to check if it has an "restrict" attribute and if yes, then attach certain other directives to the input box which is one of it's sub-children.
I have been able to layout a framework to get this done. However getting stuck at how I can dynamically attach other directives to the input box.
Plnkr here: http://plnkr.co/edit/jYiTtTQ0uPuH40zcMcer
app.directive('addOnce', ['$timeout', function($timeout){
return {
restrict: 'E',
link: function(scope, el, attrs){
if(attrs.restrict){
var input = el.find('input');
$timeout(function(){
input.val('testing');
}, 50)
// now attach directive restrict-symbols to the input box
// as a result the html would look like
//<input type="text" class="aClass" ng-model="aModel" restrict-symbols>
}
}
};
}]);
Any clues will be appreciated.
The necessary part for nested directives would be:
input.attr('restrict-symbols', '');
$compiled = $compile(input)(scope);
Take a look at the modified plunkr for the full example: http://plnkr.co/edit/HCTGj1ivp48cUXBXpn1Q?p=preview
What I did was simply to add the needed attribute to the input field, afterwards compile the element, so the nested directive gets executed. The code of the second directive is not very sophisticated but enough to demonstrate that it works
I am seeing inconsistent behavior with a directive when I $compile the element that contains the directive. In my case I have a directive that validates whether a password matches another password field. That directive looks like this:
app.directive('passwordMatches', function() {
return {
require: 'ngModel',
restrict: 'A',
scope: {
otherPasswordFieldValue: '=passwordMatches'
},
link: function (scope, elem, attrs, ngModelController) {
function validate(value) {
return value === scope.otherPasswordFieldValue;
}
//For DOM -> model validation
ngModelController.$parsers.unshift(function (value) {
var valid = validate(value);
ngModelController.$setValidity('password-matches', valid);
return valid ? value : undefined;
});
//For model -> DOM validation
ngModelController.$formatters.unshift(function (value) {
ngModelController.$setValidity('password-matches', validate(value));
return value;
});
scope.$watch(function() { return scope.otherPasswordFieldValue }, function () {
var valid = validate(ngModelController.$viewValue);
ngModelController.$setValidity('password-matches', valid);
});
}
};
});
This works fine alone. But I have another directive that is often used on the same element. The details of that directive aren't important because I've shown that the root cause of the issue is that that second directive compiles the element. As soon as I add this directive, the behavior changes. Without compiling the element, my passwordMatches directive works fine (the field becomes invalid if what I type doesn't match the other field and I can type whatever I want).
But as soon as I compile the element, I can type what I want until I make the fields match and it behaves normally up until that point. But once the values in the two fields match, if I type anything to make them not match, the field is completely blanked out. The easiest way to see this is in this jsbin: http://jsbin.com/IkuMECEf/12/edit. To reproduce, type "foo" in the first field and then try to type "fooo" (three o's) in the second field. As soon as you type the third "o" the field is blanked out. If you comment out the $compile, it works fine.
Thanks!
The second directive is compiling dom elements that have already been compiled by Angular. This second compile adds a second $watch, parser, etc because all the directive's linking functions are called again (here's a good detailed look at $compile) To confirm this you can put a console.log inside the $watch and you'll see that (with the second directive) it fires twice for every change- because of the duplicate $watch (remove the second directive and it fires only once- as expected). This second compilation step is not only causing the issue you're seeing but could cause other problems down the line.
If you have to recompile an Angular element then you first need to remove the existing one.
Here's one approach to this (explanation in the comments):
compile: function(compileElement) {
compileElement.removeAttr('another-directive');
return function(scope, element) {
// Create an "uncompiled" element using a copy of the current element's html
newe = angular.element(element.html());
// Remember where we were
parent= element.parent();
// Deleting the current "compiled" element
element.remove();
// Add the uncompiled copy
parent.append(news);
// Compile the copy
$compile(newe)(scope);
};
updated punker
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.