I have a simple html form containing regular text input. ng-minlength, ng-maxlength and ng-pattern angular built-in form input directives are set on the input.
Problem: ng-pattern check is applied before the length check by ng-minlength and ng-maxlength.
Question: how can I change the default check order: i.e. first check for the length, then apply pattern check?
Example:
<body ng-app>
<div>
<form name="myForm">
Name: <input name="name" type="text" ng-model="name" ng-minlength="3" ng-maxlength="16" ng-pattern="/^\w+$/"/>
<div ng-show="myForm.name.$dirty && myForm.name.$invalid">
<span ng-show="myForm.name.$error.pattern">Pattern error</span>
<span ng-show="myForm.name.$error.minlength || myForm.name.$error.maxlength">Length error</span>
</div>
<br/>
<input type="submit" value="Submit">
</form>
</div>
</body>
Current behavior:
enter "#" - see "Pattern error"
enter "###" - see "Pattern error"
Desired behavior:
enter "#" - see "Length error"
enter "###" - see "Pattern error"
FYI, related jsfiddle.
Thanks in advance.
Write your own directive:
var mod = angular.module("myApp", []);
mod.directive("nameValidation", function () {
return {
restrict: "A",
require: "ngModel",
link: function (scope, element, attrs, ngModelCtrl) {
var validate = function (value) {
var minLen = parseInt(attrs.myMinlength, 10),
maxLen = parseInt(attrs.myMaxlength, 10),
pattern = attrs.myPattern,
match = pattern.match(/^\/(.*)\/([gim]*)$/),
lenErr = false;
if (match) {
pattern = new RegExp(match[1], match[2]);
}
if (!ngModelCtrl.$isEmpty(value)) {
ngModelCtrl.$setValidity("pattern", true);
if ((minLen && value.length < minLen) || (maxLen && value.length > maxLen)) {
ngModelCtrl.$setValidity("length", false);
lenErr = true;
}
else {
ngModelCtrl.$setValidity("length", true);
lenErr = false;
}
if (!lenErr) {
if (match && !pattern.test(value)) {
ngModelCtrl.$setValidity("pattern", false);
}
else {
ngModelCtrl.$setValidity("pattern", true);
}
}
}
else {
ngModelCtrl.$setValidity("length", true);
ngModelCtrl.$setValidity("pattern", true);
}
}
ngModelCtrl.$parsers.push(validate);
ngModelCtrl.$formatters.push(validate);
}
}
});
Then in your HTML, include the app and use the directive:
<body ng-app="myApp">
<div>
<form name="myForm">
Name: <input name="name" type="text" ng-model="name" name-validation="" my-minlength="3" my-maxlength="16" my-pattern="/^\w+$/"/>
<div ng-show="myForm.name.$dirty && myForm.name.$invalid">
<span ng-show="myForm.name.$error.pattern">Pattern error</span>
<span ng-show="myForm.name.$error.length">Length error</span>
</div>
<br/>
<input type="submit" value="Submit">
</form>
</div>
</body>
The directive uses my-minlength, my-maxlength, and my-pattern for the three values. If length fails, that will trip first. If not, then pattern will show as error if it doesn't match. Consider renaming this directive if you want to use it other places besides name as minlength, maxlength, and pattern can be passed to it via attributes. If they are left off, they will be ignored.
See jsfiddle: http://jsfiddle.net/4zpxk/6/
I searched in angular code why this behavior. Then in the function 'textInputType' that it's the specific function that handles text inputs for the angular 'input' directive I found this at the end of this function, where we can see three blocks of code.
// pattern validator
if (pattern){
//validator logic
}
// min length validator
if (attr.ngMinlength) {
//validator logic
}
// max length validator
if (attr.ngMaxlength) {
//validator logic
}
So, no matter if you change the declaration order of your ng-* attributes in the html input element you will always get same result but if you change the order of the blocks, I mean, put the min length validator block before pattern validator block you will have the result that you expect.
This is a solution for your problem but you have to make a litte change in angular code and I don't know if you really like this. But you got a very common situation where order of the declaration of validation concepts matters, so, something more must be done to handle this. Thanks
You cannot change the default check order unfortunately.
One solution is to write a custom validator, not that difficult. Based on this answer, I came up with this code (fiddle)
Usage: There is an array of validation functions in the scope, they get passed to our custom directive "validators" as:
<input name="name" type="text" ng-model="name" validators="nameValidators"/>
A validator function would look like (e.g. for the minlength constraint):
function minlength(value, ngModel) {
if( value == null || value == "" || value.length >= 3 ) {
ngModel.$setValidity('minlength', true);
return value;
}
else {
ngModel.$setValidity('minlength', false);
return;
}
}
Important points are: it takes the value and the ngModel as arguments, performs the test (here value.length >= 3) and calls ngModel.$setValidity() as appropriate.
The directive registers the given functions with ngModel.$parsers:
app.directive("validators", function($parse) {
return {
restrict: "A",
require: "ngModel",
link: function(scope, el, attrs, ngModel) {
var getter = $parse(attrs.validators),
validators = getter(scope),
i;
for( i=0; i < validators.length; i++ ) {
ngModel.$parsers.push((function(index) {
return function(value) {
return validators[index](value, ngModel);
};
})(i));
}
}
};
});
Many details can be tweaked and improved, but the outline works (again link to fiddle). Now the order of validation is explicitly set by the order of the validator functions in the nameValidators array.
If you use ng-messages you should be able to set the order via the order of ng-message elements, e.g:
<div ng-messages="field.$error">
<ul class="validation-errors">
<li ng-message="required">This has the highest prio</li>
<li ng-message="min">Second in command</li>
<li ng-message="max">I'm last</li>
</ul>
</div>
Also the docs on this: https://docs.angularjs.org/api/ngMessages/directive/ngMessages
i just changed the order of your directives, pattern first
<input name="name" type="text" ng-model="name" ng-pattern="/^\w+$/" ng-minlength="3" ng-maxlength="16"/>
EDIT: uuum, tested your fiddel without changes and it shows your desired behavior ...
directives are compiled by priority, bbut i don't know how to set angulars directives priority ... sorry, should have tested this first
Related
Basically what I need to be able to register two types of code. The code type A has only numbers and the code Type B has numbers, one hyphen, one alphabetic value and one numeric value.
Hypothetical situation:
I register the code type A, the custom directive validate all values.
I need to register the code type B, I check the option 'Validate code
Type B', the pattern is changed in order to make the validation, then all values entered are validated.
Code - Type A
12345678
32445678
56535678
Code with complement number - Type B
32445678-a1
32445678-a2
65434567-b1
The form
<form>
<span>Insert code:</span><br/>
<input type="text" data-ng-model="code" code-type> <br/>
<input type="checkbox" data-ng-model="validateCodeTypeB" /> Validate code Type B
</form>
Note: When the 'validateCodeTypeB' option is checked the regex will be changed too. But I don't know how to implement the second regex in order to validate the code Type B.
See the example
And the directive
app.directive('codeType', function () {
return {
require: 'ngModel',
link: function (scope, element, attr, ngModelCtrl) {
function codeTypeA(text) {
if (text) {
var transformedInput = text.replace(/[^0-9]/g, '');
if (transformedInput !== text) {
ngModelCtrl.$setViewValue(transformedInput);
ngModelCtrl.$render();
}
return transformedInput;
}
return undefined;
}
ngModelCtrl.$parsers.push(codeTypeA);
}
}
});
Here I have made you plnkr for your solution:
if(!scope.codeType){
transformedInput = text.replace(/[^0-9]/g, ''); // For CodeTypeA
} else {
transformedInput = text.replace(/[^a-z]/g,''); // For CodeTypeB, change it to your own regex
}
https://plnkr.co/edit/IfIeXhy7vZacLi2jKsMs?p=preview
but regex is different for CodetypeB , change it to your regex for codetypeB and its good to go.
So I am new to Angular.
What I want to do is to force an input field (text) to be as the following:
*Uppercase only, or change the letters to uppercase automatically
*Max 30 characters
*No special characters
If someone tries to insert a special character it won't be displayed at all.
So it's not only validation.
It's accepting those rules on the input.
And I want all of this to be done on that field on a specific condition: let's say when the placeholder is equal to "Nickname" which is in my model formField.CustomFieldName
Thank you in advance
There are at least two ways you can do. Use ng-keypress to either check each character you enter or check regular expression on the input.
I am not going to give the entire solution but you can go from here.
<div ng-app="myApp">
<div ng-controller="myController">
<input type="text" ng-model="yourInput" ng-keypress="yourKeypressFunction($event)">
</div>
</div>
In your js:
var myApp = angular.module('myApp',[]);
myApp.controller('myController', ['$scope', function($scope) {
$scope.yourInput = "";
$scope.yourKeypressFunction = function(event) {
console.log(event); // check for event.which, if it is not the char you want, return;
console.log($scope.yourInput); // check for regular expression
}
}]);
https://docs.angularjs.org/api/ng/directive/ngKeypress
Try onchange event and write a method in the component that you are looking to do, or if you are using observables then try to use the observable methods to transform your input to your requirement.
Check this link http://blog.angular-university.io/introduction-to-angular-2-forms-template-driven-vs-model-driven/
This has good explanation how to deal with forms and input fields
you can do it like this using jQuery for restriction of characters
$("#youTextFieldId").keypress(function(e)
{
var code = e.which || e.keyCode;
// 65 - 90 for A-Z and 97 - 122 for a-z 95
if (!((code >= 65 && code <= 90) || !(code >= 97 && code <= 122) )
{
e.preventDefault();
}
});
and for your text limit
<input id="youTextFieldId" type="text" maxlength="30" style="text-transform:uppercase">
try to use ng-pattern=you pattern with maxlength in your text field. for eg:
<input type="text" ng-pattern="/^[a-zA-Z0-9]*$/" maxlength="30">
The /^[a-zA-Z0-9]*$/ will allow you to enter only character and numbers only. not any special characters.
Use https://github.com/candreoliveira/ngMask with a little study of documentation your pattern is very easy to create and it will also force that the user inputs the desired pattern only and on top of that you can just write maxlength="30" ,input type="text"
Look out at this example for better understanding in the demo of the ngMask ngMask String Example Demo
So this is the solution to this problem.
I hope it helps someone.
I made a fiddle for this: http://jsfiddle.net/m152d0t9/
var app = angular.module("myApp", []);
function Fiddle($scope) {}
app.directive('customInputFormat', function() {
return {
restrict: 'A',
require: '?ngModel',
scope: {
customInputFormat: '='
},
link: function(scope, elm, attr, ctrl) {
if (!ctrl) {
return;
}
scope.$watch(attr.ngModel, function(newVal, oldVal) {
if (!scope.customInputFormat) {
return;
}
if (newVal && newVal.length) {
var newStr = newVal.replace(/[^a-zA-Z0-9\s]*/g, '').toUpperCase();
if (newStr.length > 20) {
newStr = newStr.substring(0, 20);
}
ctrl.$setViewValue(newStr);
ctrl.$render();
}
})
}
};
});
And for the HTML:
<ul ng-controller="Fiddle">
<li>
<label data-ng-init="condition = true;" for="foobar"> Input text</label>
<input custom-input-format="condition" id="foobar" name="foobar" type="text" ng-model="foo" required />
</li>
</ul>
I have this custom validation directive:
/**
* Overwrites default url validation using Django's URL validator
* Original source: http://stackoverflow.com/questions/21138574/overwriting-the-angularjs-url-validator
*/
angular.module('dmn.vcInputUrl', [])
.directive('vcUrl', function() {
// 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;
var validator = function(value) {
if (!URL_REGEXP.test(value) && URL_REGEXP.test('http://' + value)) {
return 'http://' + value;
} else {
return value;
}
}
return {
require: '?ngModel',
link: function link(scope, element, attrs, ngModel) {
function allowSchemelessUrls() {
// Silently prefixes schemeless URLs with 'http://' when converting a view value to model value.
ngModel.$parsers.unshift(validator);
ngModel.$validators.url = function(value) {
return ngModel.$isEmpty(value) || URL_REGEXP.test(value);
};
}
if (ngModel && attrs.type === 'url') {
allowSchemelessUrls();
}
}
};
});
It works fine when you 'dirty' the input by typing or pasting, but I need it to run this validation, overwriting the default type="url" validation when the value is initially set in the ngModel.
I've tried adding ngModel.$formatters.unshift(validator); but it results in the 'http://' being added to input, which I need to avoid as user's changes are manually approved and it would be a waste of time to approve the addition of 'http://'.
Any help would be appreciated!
Set ng-model-options on the input type field, for example:
<input type="text"
ng-model-options="{ updateOn: 'default', debounce: {'default': 0} }"</input>
This will ensure your validator gets fired "when the value is initially set in the ngModel", as you have stated in the question.
See detailed AngularJs documentaion on ngModelOptions:enter link description here
validation of Url :
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<form name="form">
URL: <input type="url" ng-model="url.text" placeholder="Enter Link" name="fb_link"></input>
<span class="error" ng-show="form.fb_link.$error.url"></span>
</form>
Here's a jsfiddle example of what I'm trying to accomplish.
I'm trying to build a US phone number input where the view displays as (333) 555-1212, but the model binds to the numeric integer 3335551212.
My intention is to add custom validators to NgModelController which is why I have require: ng-model; there are simpler solutions without the isolate scope and NgModelController, but I need both.
You'll see an immediate error in the console: Error: Multiple directives [ngModel, ngModel] asking for 'ngModel' controller on: <input ng-model="user.mobile numeric" name="telephone" type="tel"> -- thought I was using an isolate scope here...
Thank you for looking #mimir137 but I appear to have solved it:
http://jsfiddle.net/hr121r18/8/
The directive was using replace: true, which ends up with this structure:
<form ng-controller="FooCtrl" class="ng-scope">
<p>Enter US phone number</p>
<input ng-model="user.mobile numeric" name="telephone" type="tel">
</form>
Both the template and the markup called for ng-model which led to the symptomatic error in the problem description. Once I removed that, it leads to this markup (note the wrapper element phone-number):
<form ng-controller="FooCtrl" class="ng-valid ng-scope ng-dirty ng-valid-parse" abineguid="BC0D9644F7434BBF80094FF6ABDF4418">
<p>Enter US phone number</p>
<phone-number ng-model="user.mobile" class="ng-untouched ng-valid ng-isolate-scope ng-dirty ng-valid-parse">
<input ng-model="numeric" name="telephone" type="tel" class="ng-valid ng-dirty ng-touched">
</phone-number>
</form>
But removing this required changes to $render; the elem passed into the link function is now phone-number and so you need to dig to grab the input inside it and set the value on that:
ngModel.$render = function () {
elem.find('input').val($filter('phonenumber')(ngModel.$viewValue));
};
There were a few other issues. $render() also needed to be called from the watcher.
Final:
var app = angular.module('myApp', []);
// i want to bind user.mobile to the numeric version of the number, e.g. 3335551212, but
// display it in a formatted version of a us phone number (333) 555-1212
// i am trying to make the directive's scope.numeric to have two-way binding with the controller's
// $scope.user.mobile (using isolate scope, etc.).
app.controller('FooCtrl', function ($scope) {
$scope.user = {
mobile: 3335551212
};
});
app.directive('phoneNumber', ['$filter', function ($filter) {
return {
restrict: 'E',
template: '<input ng-model="numeric" name="telephone" type="tel">',
require: 'ngModel',
scope: {
numeric: '=ngModel'
},
link: function (scope, elem, attrs, ngModel) {
// update $viewValue on model change
scope.$watch('numeric', function () {
ngModel.$setViewValue(scope.numeric);
ngModel.$render();
});
// $modelValue convert to $viewValue as (999) 999-9999
ngModel.$formatters.push(function (modelValue) {
return $filter('phonenumber')(String(modelValue).replace(/[^0-9]+/, ''));
});
// $viewValue back to model
ngModel.$parsers.push(function (viewValue) {
var n = viewValue;
if (angular.isString(n)) {
n = parseInt(n.replace(/[^0-9]+/g, ''));
}
return n;
});
// render $viewValue through filter
ngModel.$render = function () {
elem.find('input').val($filter('phonenumber')(ngModel.$viewValue));
};
}
};
}]);
app.filter('phonenumber', function () {
return function (number) {
if (!number) {
return '';
}
number = String(number);
var formattedNumber = number;
var c = (number[0] === '1') ? '1 ' : '';
number = number[0] === '1' ? number.slice(1) : number;
var area = number.substring(0, 3),
exchange = number.substring(3, 6),
subscriber = number.substring(6, 10);
if (exchange) {
formattedNumber = (c + '(' + area + ') ' + exchange);
}
if (subscriber) {
formattedNumber += ('-' + subscriber);
}
return formattedNumber;
}
});
HTML
<form ng-controller="FooCtrl">
<p>Enter US phone number</p>
<phone-number ng-model='user.mobile'></phone-number>
</form>
I created this fiddle that gets rid of most of your errors coming up in the console. Hopefully this will at least be able to put you on the right track.
I changed the template so that you can see that the filter is actually working.
It now has the typical {{ngModel | FilterName}} in plain text underneath the textbox.
The only real issue is displaying it in the textbox. I'm sure you will have no problem with that. I will check in the morning just in case you still have questions regarding this.
Edit: Alright it appears you have solved it already. Great job!
I have directive like this:
.directive('noWhitespace', ['$parse', function($parse) {
return {
restrict: 'A',
require: 'ngModel',
link: function (scope, element, attrs, ngModel) {
/*
scope.$watch(attrs.ngModel, function(value) {
var getter = $parse(value);
update(getter(scope));
});
*/
function update(viewValue) {
console.log(JSON.stringify(viewValue));
if (viewValue.match(/\s/)) {
ngModel.$setValidity('whitespace', false);
return undefined;
} else {
ngModel.$setValidity('whitespace', true);
return viewValue;
}
}
ngModel.$parsers.unshift(update);
}
};
}])
and when I use it like this:
<form name="something" novalidate>
<input ng-model="myValue" no-whitespace/>
<div ng-show="something.myValue.$error.whitespace">
Error
</div>
</form>
and I type something and then few spaces at the end update is not called until I type character after those spaces and then I got error that I have whitespace. (the same happen when I put spaces at the begining or only spaces). Why is that, and how to fix it? As you see in comments I've try to use $watch+$parse but got error Cannot read property 'match' of undefined.
Try this in your template:
<form name="something" novalidate>
<input ng-model="myValue" no-whitespace ng-trim="false"/>
<div ng-show="something.myValue.$error.whitespace">
Error
</div>
</form>
That should solve the issue of ng-model not updating when you enter empty space.
EDIT: Derp, the attribute goes in the input element not in the div ...
This is the expected behavior of model parsers in angular, the idea being that if the entered text is incorrect (has whitespace in the end), the ngModel should return undefined. When you think about it, why would you save a value to ngModel anyway if it's not correct according to your validations?
This is the relevant bit:
if (viewValue.match(/\s/)) {
ngModel.$setValidity('whitespace', false);
return undefined;
}
You are setting validity to false and returning undefined.
You can keep the model updated even if its value isn't validated by just returning viewValue always:
if (viewValue.match(/\s/))
ngModel.$setValidity('whitespace', false);
else
ngModel.$setValidity('whitespace', true);
// Return viewvalue regardless of whether it validates or not
return viewValue;