Reputation: 5204
I am building a validations module in angularjs to improve some of the basic html5 validations that come out of the box. My work is based on the jquery validations plugin. The issue I am having is that even though everything seems to be working correctly, the form isn't validating correctly. Specifically I am currently testing an email validation that is less generous than the standard html5 version in that it requires a complete email with a top level domain, but when I call $setValidity('email',false);
the form is still valid anyways. My code is below
Validations Service
'use strict';
/* Services */
function validations() {
/**
* Created by rich on 8/14/15.
*
* Most of these regex statements were kifed from jquery validation plugin with perhaps a little bit of sugar
* added to some by yours truly.
*
* Thanks to jzaeffer for his efforts.
* http://jqueryvalidation.org
* https://github.com/jzaefferer/jquery-validation
*/
var tests = {
//validates full email address with a valid domain. [string]@[string].[string]
email: function(value, param, elem) {
return /^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])+\.+(?:[a-zA-Z0-9\.](?:[a-zA-Z0-9-\.]{0,61}[a-zA-Z0-9])?)$/.test(value);
},
url: function(value, param, elem) {
return /^(?:(?:(?:https?|ftp):)?\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,})).?)(?::\d{2,5})?(?:[/?#]\S*)?$/i.test(value)
},
integer: function(value, param, elem) {
return /^(?:-?\d+|-?\d{1,3}(?:,\d{3})+)?(?:\.\d+)?$/.test(value);
},
creditcard: function(value, param, elem) {
// accept only spaces, digits and dashes
if (/[^0-9 \-]+/.test(value)) {
return false;
}
var nCheck = 0,
nDigit = 0,
bEven = false,
n, cDigit;
value = value.replace(/\D/g, "");
// Basing min and max length on
// http://developer.ean.com/general_info/Valid_Credit_Card_Types
if (value.length < 13 || value.length > 19) {
return false;
}
for (n = value.length - 1; n >= 0; n--) {
cDigit = value.charAt(n);
nDigit = parseInt(cDigit, 10);
if (bEven) {
if ((nDigit *= 2) > 9) {
nDigit -= 9;
}
}
nCheck += nDigit;
bEven = !bEven;
}
return (nCheck % 10) === 0;
},
min: function(value, param, elem) {
return value >= param
},
max: function(value, param, elem) {
return value <= param
},
between: function(value, param, elem) {
return (value >= param[0] && value <= param[1]);
},
equalTo: function(value, param, elem) {
return value === elem.val();
},
//Accept specific mime type
accept: function(value, param, elem) {
// Split mime on commas in case we have multiple types we can accept
var typeParam = typeof param === "string" ? param.replace(/\s/g, "").replace(/,/g, "|") : "image/*",
i, file;
// If we are using a wildcard, make it regex friendly
typeParam = typeParam.replace(/\*/g, ".*");
// Check if the element has a FileList before checking each file
if (elem.files && elem.files.length) {
for (i = 0; i < elem.files.length; i++) {
file = elem.files[i];
// Grab the mimetype from the loaded file, verify it matches
if (!file.type.match(new RegExp("\\.?(" + typeParam + ")$", "i"))) {
return false;
}
}
}
// Either return true because we've validated each file, or because the
// browser does not support element.files and the FileList feature
return true;
},
alphanumeric: function(value, param, elem) {
/^\w+$/i.test(value);
},
ipv4: function(value, param, elem) {
return /^(25[0-5]|2[0-4]\d|[01]?\d\d?)\.(25[0-5]|2[0-4]\d|[01]?\d\d?)\.(25[0-5]|2[0-4]\d|[01]?\d\d?)\.(25[0-5]|2[0-4]\d|[01]?\d\d?)$/i.test(value);
},
ipv6: function(value, param, elem) {
return /^((([0-9A-Fa-f]{1,4}:){7}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){6}:[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){5}:([0-9A-Fa-f]{1,4}:)?[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){4}:([0-9A-Fa-f]{1,4}:){0,2}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){3}:([0-9A-Fa-f]{1,4}:){0,3}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){2}:([0-9A-Fa-f]{1,4}:){0,4}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){6}((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b))|(([0-9A-Fa-f]{1,4}:){0,5}:((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b))|(::([0-9A-Fa-f]{1,4}:){0,5}((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b))|([0-9A-Fa-f]{1,4}::([0-9A-Fa-f]{1,4}:){0,5}[0-9A-Fa-f]{1,4})|(::([0-9A-Fa-f]{1,4}:){0,6}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){1,7}:))$/i.test(value)
},
phone: function(value, param, elem) {
return (value.length > 9 && value.match(/^(\+?1-?)?(\([2-9]([02-9]\d|1[02-9])\)|[2-9]([02-9]\d|1[02-9]))-?[2-9]([02-9]\d|1[02-9])-?\d{4}$/));
},
zip: function(value, param, elem) {
return /^\d{5}(-\d{4})?$/.test(value)
}
};
function addTest(name, test) {
tests[name] = test;
}
function isValid(test, value, params, elem) {
var valid = tests[test](value, params, elem);
return valid
}
return {
addTest: function(name, test) {
addTest(name, test);
},
test: function(testName, options) {
if (typeof options === 'object') {
return isValid(testName, options.value, options.param, options.elem);
} else {
//options is actually just the value to be tested
return isValid(testName, options);
}
},
validate: function(name, ngModel) {
var validator;
var self = this;
if (self.isType(name)) {
validator = function(ngModelValue) {
//Custom Property used for debugging
ngModel.$setValiditityTo = this.test(name, ngModelValue);
//Set validity based on results of test
ngModel.$setValidity(name, this.test(name, ngModelValue));
return ngModelValue
};
//Bind validator to current this object so that it can run the required tests.
ngModel.$parsers.push(validator.bind(self));
}
},
isType: function(name) {
var types = ['email', 'url', 'creditcard', 'integer', 'alphanumeric', 'ipv4', 'ipv6', 'phone', 'zip'];
return types.indexOf(name) > -1;
}
}
}
app.factory('validations', validations);
Input Directive
function input(validations) {
return {
restrict: 'E',
require: 'ngModel',
scope: {
type: '@'
},
link: function(scope, elem, attr, ctrl) {
validations.validate(scope.type, ctrl);
}
};
}
app.directive('input', input);
In the plunker below you can see that the custom $setValidityTo
object is set correctly, but the errors object follows the normal behavior of an email validation.
http://plnkr.co/edit/68ItZrGSlN9Yf080ctBe?p=preview
Upvotes: 0
Views: 255
Reputation: 18193
The problem is what your validation function returns when the field is invalid:
validator = function(ngModelValue) {
//Custom Property used for debugging
ngModel.$setValiditityTo = this.test(name, ngModelValue);
//Set validity based on results of test
ngModel.$setValidity(name, this.test(name, ngModelValue));
return ngModelValue
}
Above you always return the model value.
Recall that the $parsers/$formatters work as a "pipeline". The validation functions are executed in the order they are added, and if one of the validation functions deems the value to be invalid it returns undefined.
When a validation function returns undefined, the remaining of the validator functions in the pipeline are not executed.
Working plunkr, and the code that was changed:
validator = function(ngModelValue) {
var result = this.test(name, ngModelValue);
ngModel.$setValiditityTo = result;
ngModel.$setValidity(name, result);
return result ? ngModelValue : undefined;
Upvotes: 1