Overwriting the AngularJS URL validator
angularjs, validation
Solution
As of Angular 1.3, it's relatively easy now that you can completely overwrite existing `$validators`:
myApp.directive('input', function() {
function link(scope, element, attrs, ngModel) {
function allowSchemelessUrls() {
// Match Django's URL validator, which allows schemeless urls.
var URL_REGEXP = /^((?:http|ftp)s?:\/\/)(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|localhost|\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})(?::\d+)?(?:\/?|[\/?]\S+)$/i;
// Silently prefixes schemeless URLs with 'http://' when
// converting a view value to model value.
ngModel.$parsers.unshift(function(value) {
if (!URL_REGEXP.test(value) && URL_REGEXP.test('http://' + value)) {
return 'http://' + value;
} else {
return value;
}
});
ngModel.$validators.url = function(value) {
return ngModel.$isEmpty(value) || URL_REGEXP.test(value);
};
}
if (ngModel && attrs.type === 'url') {
allowSchemelessUrls();
}
}
return {
require: '?ngModel',
link: link
};
});
Problem
AngularJS accepts this for a valid URL: ``` var URL_REGEXP = /^(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?$/; ``` Django accepts this: ``` regex = re.compile( r'^(?:http|ftp)s?://' # http:// or https:// r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|' #domain... r'localhost|' #localhost... r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})' # ...or ip r'(?::\d+)?' # optional port r'(?:/?|[/?]\S+)$', re.IGNORECASE) ``` The main practical difference is that AngularJS accepts `http://some-host-without-tld`, while Django only allows `http://localhost` as a valid URL without TLD. Since it's problematic to overwrite the Django URL Validator, I want to overwrite the AngularJS validator. I attempted that this way, which works: ``` app.overwriteUrlValidator = function(ngModel) { // Django RegExp, ported to JS. var URL_REGEXP = /^(?:http|ftp)s?:\/\/(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|localhost|\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})(?::\d+)?(?:\/?|[\/?]\S+)$/gi; // Same validator as AngularJS's, only with a different RegExp. function urlValidator(value) { if (ngModel.$isEmpty(value) || URL_REGEXP.test(value)) { ngModel.$setValidity('url', true); return value; } else { ngModel.$setValidity('url', false); return undefined; } } // This is the part I'm not happy about. I need to use a timeout // because my code is executed before Angular adds its URL validator. // If I add mine before Angular does, it will not work for ??? reason. window.setTimeout(function() { ngModel.$formatters.push(urlValidator); ngModel.$parsers.push(urlValidator); }, 100); }; /** * Keep track of user's interaction with input fields */ app.inputDirective = function() { function link(scope, element, attrs, ngModel) { if (ngModel && attrs.type === 'url') { app.overwriteUrlValidator(ngModel); } } return { restrict: 'A', require : '?ngModel', link : link }; }; app.directive('input', app.inputDirective); app.directive('textarea', app.inputDirective); ``` I rather don't switch to another validation directive because I'd have to update and check a lot of code. Does anyone know a solid way of doing this?