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);
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
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).
I am a strong advocate of best practices, especially when it comes to angular but I can't manage to use the brand new $validators pipeline feature as it should be.
The case is quite simple: 1 input enhanced by a directive using $parser, $formatter and some $validators:
<input name="number" type="text" ng-model="number" number>
Here is the (simplified) directive:
myApp.directive('number', [function() {
return {
restrict: 'A',
require: 'ngModel',
/*
* Must have higher priority than ngModel directive to make
* number (post)link function run after ngModel's one.
* ngModel's priority is 1.
*/
priority: 2,
link: function($scope, $element, $attrs, $controller) {
$controller.$parsers.push(function (value) {
return isFinite(value)? parseInt(value): undefined;
});
$controller.$formatters.push(function (value) {
return value.toString() || '';
});
$controller.$validators.minNumber = function(value) {
return value && value >= 1;
};
$controller.$validators.maxNumber = function(value) {
return value && value <= 10;
};
}
};
}]);
I made a little plunk to play with :)
The behavior I am trying to achieve is: Considering that the initial value stored in the scope is valid, prevent it from being corrupted if the user input is invalid. Keep the old one until a new valid one is set.
NB: Before angular 1.3, I was able to do this using ngModelController API directly in $parser/$formatter. I can still do that with 1.3, but that would not be "angular-way".
NB2: In my app I am not really using numbers, but quantities.The problem remains the same.
It looks like you want some parsing to happen after validation, setting the model to the last valid value rather than one derived from the view. However, I think the 1.3 pipeline works the other way around: parsing happens before validation.
So my answer is to just do it as you would do it in 1.2: using $parsers to set the validation keys and to transform the user's input back to the most recent valid value.
The following directive does this, with an array of validators specified within the directive that are run in order. If any of the previous validators fails, then the later ones don't run: it assumes one validation error can happen at a time.
Most relevant to your question, is that it maintains the last valid value in the model, and only overwrites if there are no validation errors occur.
myApp.directive('number', [function() {
return {
restrict: 'A',
require: 'ngModel',
/*
* Must have higher priority than ngModel directive to make
* number (post)link function run after ngModel's one.
* ngModel's priority is 1.
*/
priority: 2,
link: function($scope, $element, $attrs, $controller) {
var lastValid;
$controller.$parsers.push(function(value) {
value = parseInt(value);
lastValid = $controller.$modelValue;
var skip = false;
validators.forEach(function(validator) {
var isValid = skip || validator.validatorFn(value);
$controller.$setValidity(validator.key, isValid);
skip = skip || !isValid;
});
if ($controller.$valid) {
lastValid = value;
}
return lastValid;
});
$controller.$formatters.push(function(value) {
return value.toString() || '';
});
var validators = [{
key: 'isNumber',
validatorFn: function(value) {
return isFinite(value);
}
}, {
key: 'minNumber',
validatorFn: function(value) {
return value >= 1;
}
}, {
key: 'maxNumber',
validatorFn: function(value) {
return value <= 10;
}
}];
}
};
}]);
This can be seen working at http://plnkr.co/edit/iUbUCfJYDesX6SNGsAcg?p=preview
I think you are over-thinking this in terms of Angular-way vs. not Angular-way. Before 1.3 using $parsers pipeline was the Angular-way and now it's not?
Well, the Angular-way is also that ng-model sets the model to undefined (by default) for invalid values. Follow that Angular-way direction and define another variable to store the "lastValid" value:
<input ng-model="foo" ng-maxlength="3"
ng-change="lastValidFoo = foo !== undefined ? foo : lastValidFoo"
ng-init="foo = lastValidFoo">
No need for a special directive and it works across the board in a way that doesn't try to circumvent what Angular is doing natively - i.e. the Angular-way. :)
As of Angular 1.3 you can use the ngModelOptions directive to have greater control as to when your model value updates. Take a look at this updated Plunker to show you how to achieve the functionality you are looking for: http://plnkr.co/edit/DoWbvlFMEtqF9gvJCjPF?p=preview
Basically you define the model as a getterSetter and only return the new value if it is valid:
$scope.validNumber = function(value) {
return angular.isDefined(value) ? ($scope.number = value) : $scope.number;
}
$scope.modelOptions = {
getterSetter: true,
allowInvalid: false
};
Then to use this code update your as follows:
<input name="number" type="text" ng-model="validNumber" ng-model-options="modelOptions" number>
I really hope this answers all of your questions, please let me know if I can help any more.
Leon.
Here is my plnkr with the relevant code:
$controller.$$runValidators = function(originalRun) {
var lastModelValue, lastViewValue;
return function() {
var ctrl = this;
var doneCallback = arguments[arguments.length-1];
arguments[arguments.length-1] = function(allValid) {
doneCallback(allValid);
console.log(allValid);
console.log('valid:' +allValid+ ' value:' +ctrl.$viewValue);
if (ctrl.$viewValue) {
lastViewValue= allValid ? ctrl.$viewValue : lastViewValue | '';
lastModelValue= allValid ? ctrl.$modelValue : lastModelValue;
ctrl.$modelValue = allValid ? ctrl.$modelValue : lastModelValue;
ctrl.$$writeModelToScope();
ctrl.$viewValue = ctrl.$$lastCommittedViewValue = lastViewValue;
ctrl.$render();
}
console.log(ctrl.$viewValue + ' '+lastViewValue);
// console.log( ctrl.$modelValue);
};
originalRun.apply(this, arguments);
}
}($controller.$$runValidators);
Can it be a valid solution?
the only way i think you can intercept the angular validation flow is override the $$runValidators. Maybe this code need a little bit of tweaking but works.
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/