In AngularJS both $parsers and $validators can be used to validate forms. I was wondering what exactly the difference is between using a $parser and using a $validator.
Let's look at the following example:
Validation using a parser
angular.module("myFormApp")
.directive("containsWhiteSpace", containsWhiteSpace);
function containsWhiteSpace () {
function hasWhiteSpace(s) {
return s.indexOf(' ') >= 0;
}
return {
require: "ngModel",
link: function(scope, ele, attrs, ctrl){
ctrl.$parsers.unshift(function(value) {
if(value){
var isValid = !hasWhiteSpace(value);
ctrl.$setValidity('hasWhiteSpace', isValid);
}
return isValid ? value : undefined;
});
}
}
}
Validation using a validator
angular.module("myFormApp")
.directive("containsWhiteSpace", containsWhiteSpace);
function containsWhiteSpace () {
function hasWhiteSpace(s) {
return s.indexOf(' ') >= 0;
}
return {
require: "ngModel",
link: function(scope, ele, attrs, ctrl){
ctrl.$validators.hasWhiteSpace = function(value){
return !hasWhiteSpace(value);
}
}
}
}
They both do the same thing, but when is it correct to use a parser and when is it correct to use a validator? What are the advantages of both? What are the differences?
$parsers are run in a pipeline, to transform the input value. They can return undefined if they fail to parse the input, indicating a parse error. If the input value fails to parse, the validators are not run.
$validators are run after parsers, on the parsed value. They can return false to indicate a data error.
Basically
use parsers to coerce a string input value into the data type you need/expect.
use validators to validate a value of the correct data type.
For example, consider a model requiring a positive number. $parsers can call parseFloat on the value, and $validators can check if the value is positive.
For the example you gave, I think $validators are more appropriate (and cleaner).
Related
I'm trying to create a directive named currency that appends a $ before the text in input. The dollar sign should be shown at all times and shouldn't be possible to remove.
Here's my code:
app.directive('currency', function() {
return {
restrict: 'A',
require: 'ngModel',
link: function (scope, elem, attrs, controller) {
// view -> model
controller.$parsers.push(function (viewValue) {
viewValue = viewValue.replace(/^\$/, '');
controller.$viewValue = viewValue;
return viewValue;
});
// model -> view
controller.$formatters.push(function (modelValue) {
modelValue = '$' + modelValue;
controller.$modelValue = modelValue;
return modelValue;
});
}
};
});
Working example: https://jsfiddle.net/U3pVM/29012/
As you can see, the dollar sign is appended initially, but can be deleted and won't be appended after that. It seems that the function I push to $formatters is only being called once. Is it supposed to work like that or am I missing something? How can I implement the desired behavior?
ok, i have tried a workaround, it works but i am not sure if this is the correct way to do it.
updated fiddle : https://jsfiddle.net/U3pVM/29014/
controller.$parsers.push(function (viewValue) {
//console.log(viewValue.substring(0,1));
if(viewValue.substring(0,1) != "$"){
var view_value = "$" + viewValue;
controller.$setViewValue(view_value);
controller.$render();
}
viewValue = viewValue.replace(/^\$/, '');
//controller.$viewValue = viewValue;
console.log(viewValue);
return viewValue;
});
P.S: i am not sure why you are injecting ngModel as controller in your link function. it might be a mistake.
I think you're not quite understanding what the $parsers and $formatters do. Whenever you enter something in the input field, the $parsers are responsible for converting this value into a model value. Formatters are responsible for converting a model value into a display value in your input field.
What you are attempting to do is to change the content of your input field ($formatter feature) when someone enters something into the field ($parser feature).
While I'm sure there are workarounds to making it work this way, you're misusing the concepts of $parsers and $formatters when you do. Instead you should be looking at a custom directive (or extend the one you have) to add to the input that does what you're trying to do, for instance by handing keyups.
Edit
See the following code example for a link function to give you some indication of what I mean:
link: function (scope, elem, attrs, controller) {
elem.bind('keyup', function(evt) {
// Change the displayed value after every keypress
// This function is an example and needs serious work...
// Perhaps you should even put this in a separate directive
var value = elem.val().replace(/[^$0-9]/g, '');
if (value && value.substring(0,1) !== '$') {
value = '$' + value;
}
elem.val(value);
});
// view -> model
controller.$parsers.push(function (viewValue) {
// Any time the view changes, remove the $ sign and interpret the rest as number for the model
var modelValue = viewValue.replace(/^\$/, '');
return parseFloat(modelValue);
});
// model -> view
controller.$formatters.push(function (modelValue) {
// Any time the model (number) changes, append it with a $ sign for the view
var viewValue = '$' + modelValue;
return viewValue;
});
}
Or check the entire fiddle: https://jsfiddle.net/cL0hpvp4/
I have a lovely date dropdown that works in AngularJS V1.0.8 and I am trying to run it using V1.4.2 but it doesn't seem to want to play ball.
What seems to be the issue? As I have researched changes in the versions but can't see the problem.
I have got a plunker where you can see it working using 1.0.8 and then when you change the version on lines 5 and 8 to 1.4.2 it doesn't work.
The fields should become invalid for dates like 31/02/2000 etc which is fine but not in 1.4.2
What can the matter be?
https://plnkr.co/edit/5ckBkzN6xYEvJvyoO0Ax?p=preview
angular.module('dateApp', []);
angular.module('dateApp').
directive('dateTypeMulti', function () {
return {
require: 'ngModel',
link: function (scope, element, attrs, ngModel) {
ngModel.$render = function () {
angular.extend(scope.$eval(attrs.dateTypeMulti), ngModel.$viewValue);
};
scope.$watch(attrs.dateTypeMulti, function (viewValue) {
ngModel.$setViewValue(viewValue);
}, true);
ngModel.$formatters.push(function (modelValue) {
if (!modelValue) return;
var parts = String(modelValue).split('/');
return {
year: parts[0],
month: parts[1],
day: parts[2]
};
});
ngModel.$parsers.unshift(function (viewValue) {
var isValid = true,
modelValue = '',
date;
if (viewValue) {
date = new Date(viewValue.year, viewValue.month - 1, viewValue.day);
modelValue = [viewValue.year, viewValue.month, viewValue.day].join('/');
if ('//' === modelValue) {
modelValue = '';
} else if (
date.getFullYear() != viewValue.year ||
date.getMonth() != viewValue.month - 1 ||
date.getDate() != viewValue.day) {
isValid = false;
}
}
ngModel.$setValidity('dateTypeMulti', isValid);
return isValid ? modelValue : undefined;
});
}
};
})
Thanks
Your directive stops working in 1.3.0-beta.10, probably because of the following change:
ngModel: do not dirty the input on $commitViewValue if nothing was
changed
Since you are using an object as the view value:
scope.$watch(attrs.dateTypeMulti, function (viewValue) {
ngModel.$setViewValue(viewValue);
}, true);
The same object reference will be used, $commitViewValue will deem nothing to have changed and abort before the parse and validate pipeline gets started.
The documentation for $setViewValue states:
When used with standard inputs, the view value will always be a string
(which is in some cases parsed into another type, such as a Date
object for input[date].) However, custom controls might also pass
objects to this method. In this case, we should make a copy of the
object before passing it to $setViewValue. This is because ngModel
does not perform a deep watch of objects, it only looks for a change
of identity. If you only change the property of the object then
ngModel will not realize that the object has changed and will not
invoke the $parsers and $validators pipelines. For this reason, you
should not change properties of the copy once it has been passed to
$setViewValue. Otherwise you may cause the model value on the scope to
change incorrectly.
Change to use angular.copy and it should work:
ngModel.$setViewValue(angular.copy(viewValue))
Demo: https://plnkr.co/edit/kSS56n6LlHej25vcjfQq?p=preview
I would expect the following expression to have the same outcome:
Case 1:
<input type="range" name="myRangeInput" ng-model="value.rangeInput" value="value.rangeInput" min="-55" max="55">
Case 2 (difference to case 1 is that I replaced 55 with AngularJS scope variables):
<input type="range" name="myRangeInput" ng-model="value.rangeInput" value="value.rangeInput" min="{{value.rangeInputMin}}" max="{{value.rangeInputMax}}">
with value.rangeInputMax equals 55 and value.rangeInputMin equals -55.
But they do not have the same output. For example let's says value.rangeInput is in both cases -10. Then in the 1st example the dot in the range slider is set at -10. But in the 2nd example the dot is set to 0.
I tried to convert value.rangeInputMin and value.rangeInputMax into numbers and change the statement (without double quotes) to this:
<input type="range" name="myRangeInput" ng-model="value.rangeInput" value="value.rangeInput" min={{value.rangeInputMin}} max={{value.rangeInputMax}}>
I also tried with different notations, e.g. value.rangeInputMin, "value.rangeInputMin", tried to set it with ng-init, create another scope variable and assign the value in this one, etc.
But it is still showing a different behaviour than in the 1st case.
Per my comments above, I think you've found a bug as I'd expect to be able to declaratively set this value in your template alongside your min and max values. This simply isn't the case. A typical workaround for this is to set the model value after you've set your min and max values using $timeout. It's not ideal but it works.
controller function
function($timeout) {
var vc = this
// vc.rangeInput = -10;
vc.rangeInputMin = -55;
vc.rangeInputMax = 55;
$timeout(function(){
vc.rangeInput = -10;
})
}
You can see it working here - http://codepen.io/jusopi/pen/vLQKJY?editors=1010
If you need to, you can write a simple directive to basically trigger ng-init-like functionality on the next $digest cycle. This might be a better solution if you run into this issue more than once of twice in your design.
callLater directive
.directive('callLater', [
'$timeout',
function($timeout) {
return {
restrict: 'A',
scope: {
callLater: '&'
},
link: function($scope, elem, attr) {
$timeout(function(){
$scope.callLater()
})
}
}
}
])
directive in template
<input type="range" name="myRangeInput" ng-model="vc.delayedInput"
min="{{vc.rangeInputMin || -55}}" max="{{vc.rangeInputMax || 55}}"
call-later="vc.delayedInput = -10;">
example - http://codepen.io/jusopi/pen/JGeKOz?editors=1010
The problem is commented here: https://github.com/driftyco/ionic/issues/1948
JWGmeligMeyling created the ngMax and ngMin directives and they seem to work pretty well:
.directive('ngMin', function() {
return {
restrict : 'A',
require : ['ngModel'],
compile: function($element, $attr) {
return function linkDateTimeSelect(scope, element, attrs, controllers) {
var ngModelController = controllers[0];
scope.$watch($attr.ngMin, function watchNgMin(value) {
element.attr('min', value);
ngModelController.$render();
})
}
}
}
})
.directive('ngMax', function() {
return {
restrict : 'A',
require : ['ngModel'],
compile: function($element, $attr) {
return function linkDateTimeSelect(scope, element, attrs, controllers) {
var ngModelController = controllers[0];
scope.$watch($attr.ngMax, function watchNgMax(value) {
element.attr('max', value);
ngModelController.$render();
})
}
}
}
})
Here's the codepen: http://codepen.io/anon/pen/MKzezB
Try removing value="value.rangeInput" from the markup.
I have an angularjs app, which validates certain input fields. I was looking to write unit tests via Jasmine to test and maintain the validity of these fields.
NOTE: The validation works fine normally, just with jasmine, it doesn't seem to update.
The unit tests have no syntax errors, but simply result in:
Error: Expected false to equal true.
at new jasmine.ExpectationResult
at null.toEqual
at null.<anonymous>
at jasmine.Block.execute
at jasmine.Queue.next_
at chrome-extension
For instance, I have, in the directives:
}).directive('billingNumberPopup', function() {
return {
require: 'ngModel',
link: function(scope, elm, attrs, ctrl) {
scope.$watch(
function() {
return ctrl.$viewValue;
},
function(value){
numValidation(value);
}
);
function numValidation(viewValue){
if (!viewValue || viewValue == "" || (!viewValue.toString().match(/[a-z]/gi) && viewValue.toString().match(/[0-9]/g).length == 6)){
ctrl.$setValidity('billingNumber',true);
}
else
{
ctrl.$setValidity('billingNumber',false);
}
and then from my unit tests...
it('Check if validation works', function(){
var len = $scope.dataToPost.length;
$scope.addRow();
console.log("Hi");
$scope.$apply(function(){
$scope.dataToPost[len].billingNumber = "HELLO";});
$scope.$apply();
console.log($scope.dataToPost[len].billingNumber);
console.log($("input[ng-model='d.billingNumber']"));
expect($("input[ng-model='d.billingNumber']")[len].classList.contains("ng-invalid")).toEqual(true);
});
where "HELLO" is not a valid billing number, and scope.dataToPost is the data that is binded to the input fields. I would assume, that changing the value, and calling $scope.$apply would trigger validation, any suggestions?
The jasmine error indicates that you are trying to access a null object.
This appears to occur when you are accessing the len instead of the len - 1 index of the array. Try changing the expectation to:
expect($("input[ng-model='d.billingNumber']")[len - 1].classList.contains("ng-invalid")).toEqual(true);
I am writing a directive which will be used to establish client and server-side validation on an input. It should accept an array of validator names (e.g. aa-validate="required,unique"), loop through them, add client-side validation directives for all possible validators (e.g. required should add ngRequired), and for the rest, post to a server-side validation API.
The last part of that works well: I am watching the ngModel attribute, and posting to the server with a 100ms timeout. However, setting client-side validation directives from within the linking function of my directive does NOT cause them to be compiled and linked. In other words, they do nothing. Here is my code:
angular.module('form', [])
.directive('aaValidate', ['$http', function($http) {
return {
priority: 1,
restrict: 'A',
require: 'ngModel',
link: function(scope, element, attrs, ctrl) {
var validate = attrs.aaValidate,
validators = validate.split(',');
// This is the problem!
//
// Populate possible client-side validators
for (var i = 0, len = validators.length; i < len; i++) {
var validator = validators[i];
switch (validator) {
case 'required':
attrs.$set('ngRequired', 'true'); break;
// ... and so on for ngPattern, etc.
default: break;
}
}
scope.$watch(attrs.ngModel, function(value) {
// This part works!
//
// Clear existing timeout, reset it with an
// $http.post to my validation API, the result is
// passed into ctrl.$setValidity
});
}
}
}]);
I did make an attempt to inject $compile, and re-compile the element at the end of the linking function. I ended up with infinite recursion, likely because I failed to remove some attributes, but even if I manage to do it this way, it feels rather ugly. What is the correct approach?
Any help is greatly appreciated. Thanks in advance.
EDIT: jsFiddle: http://jsfiddle.net/3nUdj/4/
My first answer was wrong - I don't think there's any way around using the $compile service. Here's how you can do it without getting infinite recursion. I basically split the directive in two directives - one adds the validation directives, removes itself and recompiles. The other does the other stuff:
angular.module('form', [])
.directive('aaValidate', ['$http', '$compile', function ($http, $compile) {
return {
link: function (scope, element, attrs) {
var validate = attrs.aaValidate,
validators = validate.split(',');
// Populate possible front-end validators
for (var i = 0, len = validators.length; i < len; i++) {
var validator = validators[i];
switch (validator) {
case 'required':
attrs.$set('ngRequired', 'true');
break;
default:
break;
}
}
attrs.$set('aaOther', '');
element.removeAttr('aa-validate');
$compile(element)(scope);
}
}
}])
.directive('aaOther', function () {
return {
require: 'ngModel',
link: function (scope, element, attrs, ctrl) {
scope.$watch(attrs.ngModel, function (value) {
// Server-side validation
});
}
}
});
You have to recompile the linked element for this to work in ng-repeat. I've updated the fiddle: http://jsfiddle.net/3nUdj/7/