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
Trying to write a angular model that allows for a two way binding, I.E. where it has a ng-model variable, and if the controller updates it, it updates for the directive, if the directive updates it, it updates for the controller.
I have called the controller scope variable ng-model, and the directive model bindModel. In this case it sends an array of data, but that should not make a difference to how the binding works (I think at least).
So first the mapping, this is how the initial directive looks (link comes later).
return {
link: link,
scope: { cvSkills: '=',
bindModel:'=ngModel'},
restrict: 'E'
}
My understanding is (and I am uncertain about the exact scope at this moment) the directive will be aware of cvSkills (called cvSkills internally, and used to provide intial data), and bindModel which should pick up whats in ng-model).
The call in html is:
<skilltree cv-skills="cvskills" ng-model="CV._skillSet"></skilltree>
So the variables aren't actually (quite) in the directive yet. But there is an awareness of them, so I created two watchers (ignoring the cvskills one for now)
function link(scope,element, attr){
//var selected = ["55c8a069cca746f65c9836a3"];
var selected = [];
scope.$watch('bindModel', function(bindModel){
if (bindModel.length >> 0) {
console.log("Setting " + bindModel[0].skill)
selected = bindModel;
}
})
scope.$watch('cvSkills', function(cvSkills) {
So once the scope.$watch sees an update to bindModel, it picks it up and stores it to selected (same happens separately with cvSkills). In cvskills I then do updates to selected. So it will add additional data to selected if the user clicks buttons. All works and nothing special. My question is now. How do I then update bindModel (or ngModel) when there are updates to selected so that the controllers scope picks it up. Basically, how do I get the updates to propagate to ng-model="CV._skillSet"
And is this the right way to do it. I.E. make the scope aware, than pick up changes with scope.$watch and manually update the variable, or is the a more "direct" way of doing it?
=================== Fix using ngModel ===================
As per the article, if you add require: "ngModel" you get a fourth option to the function (and skip having a binding between ngModel and bindModel).
return {
link: link,
require: 'ngModel',
scope: { cvSkills: '='},
restrict: 'E'
}
Once you have done that, ngModel.viewValue will contain the data from ngModel= , in my case I am updating this in the code (which may be a bad idea). then call ngModel.setViewValue. It is probably safeish if you set the variable and then store it straight away (as follows)
function link(scope,element, attr, ngModel){
ngModel.$render = function() {
console.log("ngRender got called " + ngModel.$viewValue );
} ;
....
ngModel.$viewValue.push(newValue);
ngModel.$setViewValue(ngModel.$viewValue)
================= If you don't care about viewValue ==================
You can use modelValue instead of viewValue, and any updates to modelValue is propagated straight through.
function link(scope,element, attr, ngModel){
ngModel.$render = function() {
console.log("ngRender got called " + ngModel.$modelValue );
} ;
....
ngModel.$modelValue.push(newValue);
Using the require attribute of the directive you can have the controller API from ngModel directive. So you can update values.
Take a look at this very simple demo here : https://blog.hadrien.eu/2015/01/16/transformation-avec-ngmodel/
For more information here is the documentation : https://docs.angularjs.org/#!/api/ng/type/ngModel.NgModelController
I am experiencing a strange issue in that my directive seems to be executing on stale row.entities, meaning it does not get the new values as you scroll down or modify the sort of the grid. The initial ~20 rows render fine but past that the directives become disassociated with the rows.
See my very hacked together example here.
It looks like during sort values of the expression you pass to directive change, but the expression itself stays the same.
You should change scope & expression binding to = value binding (and access the value with scope.installs, without function call), then you will be able to track the changes.
// ...
scope: {
installs: '='
},
// ...
Then, to track the changes you can use scope.$watch and put your code inside.
link: function (scope, element, attrs) {
scope.$watch('installs', function(newValue) {
// your code, you can use newValue as current installs value
var installs = newValue;
// ...
});
}
Example here.
I am trying to create a directive that will replace itself with the ng-pattern attribute. The attribute gets applied to the input element but the element basically becomes unusable after that. I can no longer enter characters into the text box.
Here is the plunkr
http://plnkr.co/edit/F6ZQYzxd8Y04Kz8xQmnZ?p=preview
I think I must be compiling the element incorrectly after the attribute is added.
app.directive('passwordPattern', ['$compile', function($compile){
return{
compile: function (element, attr){
element.removeAttr('password-pattern');
element.attr('ng-pattern', '/^[\\w\\S]{6,12}$/');
return {
pre: function preLink(scope, iElement, iAttrs, controller) { },
post: function postLink(scope, iElement, iAttrs, controller) {
$compile(iElement)(scope);
}
};
}
};
}]);
Any thoughts on a solution or why the textbox becomes unusable would be greatly apprecitated. Thanks.
In addition to priority: 1000, you need to add terminal: true.
The issue is that without terminal: true, the input directive gets compiled twice, and 2 sets of change listeners are getting added, which throws the ngModel directive logic off a bit.
The first compile Angular performs doesn't see the ng-pattern, so the input directive doesn't add the validateRegex parser to its list of parsers. However, the second compile (via your $compile(iElement, scope)) sees the ng-pattern and does add the validateRegex parser.
When you type, say 3, into the input box, the first change listener is called and sees the number 3. Since no ng-pattern was applied (which would've added the validateRegex $parser), no $parsers exist and the model is updated with 3 immediately.
However, when the second change listener is called, it sees the ng-pattern and calls validateRegex, which calls ngModel.$setValidity('pattern', false) and returns undefined (because the model should never be set to an invalid value). Here's the kicker - inside the ngModel directive, since the previous $viewValue of 3 and new value of undefined are out of sync, Angular calls the input directive's $render function, which updates the input to be empty. Thus when you type 3 (or anything) into the input box, it's immediately removed and appears to be broken.
A high priority (like 1000) and terminal: true will prevent the input directive (and most likely other directives unless you have one that's priority: 1001+) from being compiled the first time. This is great because you want the input directive to take into account ng-pattern - not without it in place. You don't want multiple sets of change listeners added to the same element or it may (and will) cause strange side-effects.
Another solution will be to override the pattern property of the $validators object in ngModel controller.
You can see an example of a validator function in ngModelController docs
Here's an example in a directive:
angular.module('mymodule')
.directive('mydirective', MyDirective);
function MyDirective() {
return {
restrict: 'A',
require: 'ngModel',
scope: {},
link: function(scope, element, attrs, ngModelController) {
ngModelController.$validators["pattern"] = validatePattern;
function validatePattern(modelValue, viewValue) {
var value = modelValue || viewValue;
return /[0-9]+/.test(value);
}
}
}
}
You can modify the above example to receive the pattern from the outside scope and change the validation function using a scope.$watch on the pattern.
I try to create a directive which should peform some actions when an input field is marked as invalid. For this example lets assume I have a directive which checks if the input is a prime number, and I want to create a directive which adds a class to the element when it's invalid:
<input type="text" ng-model="primeNumber" validate-prime invalid-add-class="error">
The validate-prime uses the parsers and formatters on ng-model to update the validity of the model.
Now I want the invalid-add-class directive to add the class "error" when the model is invalid, and to remove it when it is valid. In other words, it should watch the $valid (or $invalid) property of the model controller. However, I can't figure out how to get this working. I tried:
link : function(scope, element, attrs, ctrl) {
ctrl.$watch("$valid", function(newVal, oldVal) {
//never fired
});
}
Perhaps I could watch some variable on scope, but I don't know which variable to watch for.
So how can I be notified when the validity of a model changes?
If you have a <form>, add a name to it (lets assume 'myForm') and a name to your input (lets assume myInput). You should be able to $watch this by:
scope.$watch('myForm.myInput.$valid', function(validity) {})
If you don't have a form, you can always watch a function. This way:
scope.$watch(function() { return ctrl.$valid; }, function(validity){});
You can read more about the form approach here.
If you do not have a <form />you can easily get one:
In your directive definition:
require: '^form'
and then in your link function, the form is passed as the fourth parameter:
link: function (scope, element, attr, ctrl) {
Now you don't have to hard-code the form or the input field to perform the $watch:
scope.$watch(ctrl.$name + '.' + element.attr('name') + '.$valid',
function (validity) {});
Our goal, in general, should be to make a directive work independently of any one form or input. How can we allow it to read the local $valid property without imperatively binding it to a single specific form & input name?
Just use require: 'ngModel' as one of the properties of your directive config. This will inject the local ngModel controller as the fourth argument to the link function, and you can place a $watch directly upon $valid without needing to couple the directive's implementation to any particular form or input.
require: 'ngModel',
link: function postLink(scope, element, attrs, controller) {
scope.inputCtrl = controller;
scope.$watch('inputCtrl.$valid', handlerFunc)
}
The handler should consistently fire on changes to $valid with that structure. See this Fiddle, where the input is validated for the pattern of a U.S. Zip-Code or Zip+4. You'll get an alert each time validity changes.
EDIT 3/21/14: This post previously got hung up on a delusion of mine, fixating on the wrong cause of an implementation problem. My fault. The example above removes that fixation. Also, added the fiddle, showing that this approach does in fact work, and always did, once you add quotes around the watch expression.
From the directive Angular docs, I see the compile function has 3 parameters, one of which is transclude. The only explanation the docs provide is:
transclude - A transclude linking function: function(scope, cloneLinkingFn).
I'm trying to understand what exactly you would do in the clone linking function. I don't even know what parameters get passed into it. I found one example that has one parameter called clone that appears to be an HTML element. Are there other parameters available? Which HTML element is this exactly? I'm also looking at probably using transclude: 'element' in my directive. Do the answers to those questions change when using 'element' instead of true?
I'm understanding transclusion with the simple examples, but I can't to seem to find more complex examples, especially with transclude: 'element'. I'm hoping someone can provide a more thorough explanation about all this. Thanks.
EDIT: Completely and totally changing my answer and marking this as "Community Wiki" (meaning no points for me) as I was outright wrong when I answered this
As #Jonah pointed out below, here is a really good article on the compile option of directives and using the transclusion function
The basic idea is the compile function should return a linking function. You can use the transclusion function provided inside the linking function to take a clone of the transcluded DOM element, compile it, and insert it wherever it needs to be inserted.
Here is a better example I've pulled out of my butt on Plunker
The idea of the compile function is it gives you a chance to programmatically alter the DOM elements based on attributes passed BEFORE the linking function is created and called.
// a silly directive to repeat the items of a dictionary object.
app.directive('keyValueRepeat', function ($compile){
return {
transclude: true,
scope: {
data: '=',
showDebug: '#'
},
compile: function(elem, attrs, transclude) {
if(attrs.showDebug) {
elem.append('<div class="debug">DEBUG ENABLED {{showDebug}}</div>');
}
return function(scope, lElem, lAttrs) {
var items = [];
console.log(lElem);
scope.$watch('data', function(data) {
// remove old values from the tracking array
// (see below)
for(var i = items.length; i-- > 0;) {
items[i].element.remove();
items[i].scope.$destroy();
items.splice(i,1);
}
//add new ones
for(var key in data) {
var val = data[key],
childScope = scope.$new(),
childElement = angular.element('<div></div>');
// for each item in our repeater, we're going to create it's
// own scope and set the key and value properties on it.
childScope.key = key;
childScope.value = val;
// do the transclusion.
transclude(childScope, function(clone, innerScope) {
//clone is a copy of the transcluded DOM element content.
console.log(clone);
// Because we're still inside the compile function of the directive,
// we can alter the contents of each output item
// based on an attribute passed.
if(attrs.showDebug) {
clone.prepend('<span class="debug">{{key}}: {{value}}</span>');
}
//append the transcluded element.
childElement.append($compile(clone)(innerScope));
});
// add the objects made to a tracking array.
// so we can remove them later when we need to update.
items.push({
element: childElement,
scope: childScope
});
lElem.append(childElement);
}
});
};
}
};
});