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.
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'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 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 want to have an attribute directive that can be used like this:
<div my-dir="{items:m.items, type:'0'}"></div>
In my directive I can turn this into an object like:
function (scope, element, attributes, modelController)
{
var ops = $.extend({}, scope.$eval(attributes.myDir));
}
Now say m.items is an array. And somewhere down the line an ajax call replaces the array with a new one. I'd like to watch for this.
How can I watch m.items or whatever was typed in for the items property in the attribute? What should the watch expression be?
You should be able to use scope.$watch to watch the attribute. Angular will invoke the callback with the entire object when any of the values change.
function(scope, elem, attrs) {
scope.$watch(attrs.myDir, function(val) {
// Do something with the new value, val.
}, true);
}
http://plnkr.co/edit/JBGqW8uFzh1eJ1Ppq3fT
Anytime the model changes (by typing something in the input) the directive updates the elements html in the $watch.
Edited my answer: you want to include true as the last argument to compare object values, not references. Otherwise you will run digest more than 10 times and get an exception.
scope.$watchCollection (watchExpression, listener)
Refer to: https://docs.angularjs.org/guide/scope