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.
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
This is a follow-up on https://stackoverflow.com/a/24058594/261673 where I asked how to make a directive that provides default options for ng-options directive.
I'm trying to provide a default value in case the value defined by ng-model is not defined.
I've added require: 'ngModel' to the directive options and the following code to link function returned by compile function:
if(!$ctrl.$viewValue) {
$ctrl.$setViewValue($scope.months[0]);
}
where $ctrl is ngModelController. It works fine (see edit) if I set priority to 0, however, if I leave it as it originally was (1001) it no longer works. I suspect that this directive is compiled before ngModel and because it's terminal ngModel is never compiled and something breaks.
Is there a way to somehow make it work without changing priority to 0?
Here's Plunker with the whole code: http://plnkr.co/edit/IZ0eyqHoqwSVPlhxI98X?p=preview
EDIT: Actually, it doesn't work fine with priority 0 as well. For some reason every second time the option in any select is changed, the value gets undefined and there are empty options with ? undefined:undefined ? and ? values...
I don't know why the original answerer thought they have to use terminal: true, but you don't have to.
All you need it to make sure your directive compiles before the select directive (which executes at priority 0), so any priority >0 will do (in fact using priority 1 works fine).
Then you can access the required directive's controller as usual:
app.directive('myOptionsMonths', function ($timeout) {
return {
priority: 1,
require: 'ngModel',
compile: function (tElem, tAttrs) {
tAttrs.$set('ngOptions', 'month for month in months');
tAttrs.$set('myOptionsMonths', null);
return function (scope, elem, attrs, ctrl) {
scope.months = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];
$timeout(function () {
if (ctrl.$viewValue === undefined) {
ctrl.$setViewValue(scope.months[0]);
}
});
};
}
};
});
You still need to use $timeout, because the NgModelController's properties are not set yet
See, also, this short demo.
I think I've figured it out, however, it looks like a terrible hack.
Looking at the source code of $compileProvider.directive function, I've found a way to access ngModelController without using require: 'ngModel':
$timeout(function () {
var ctrl = $element['data']('$ngModelController');
if (ctrl.$viewValue) {
ctrl.$setViewValue($scope.months[0]);
}
});
It's very similar to the previous code, but this time it gets access to ngModelController via compiled element. $timeout is need, because otherwise ctrl.$viewValue was always NaN at this point, even if model value was set, so it was always overwriting model value.
Here's updated Plunker: http://plnkr.co/edit/h5ZPqTdyrZ0zTyyDcdei?p=preview
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',
...
};
});
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
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.