AngularJS Validation ngModel undefined - angularjs

I have my form validations dynamically added to the form from a response to a web service call. When the call returns it tells my validation directive what validations I need to add to the form. (I do this because I want to reuse the same validations on the server during submit as I do on the client.) This works wonderfully when the only validations are of type "required". The problem I have is when the model value of the form control does not pass the the validation rules, the model value is then "undefined". Therefore nothing get's sent to the server on form submission to validate on the server side. I do realize I could block the form submission if the form is not valid, however, I am letting the server determine the validity of the data that comes across.
What could I do to force the model value to be the "invalid value" regardless if it violated a validation rule? Better suggestions? Below is a snipit of my directive I am using.
//this directive should be put on an ng-form element and it will hide/show any validations set on each input
.directive('formValidator', ['validatorService', function (vs) {
return {
restrict: 'A',
require: '^form',
link: function (scope, element, attrs, form) {
function iterateOverErrorsObject(errors, func, ignorechecking) {
if (!func)
return;
//show any new errors
for (var key in errors) {
if (key.indexOf('__') == 0)
continue;
_.each(errors[key], function (obj) {
if (form[obj.$name] == obj || ignorechecking) { //ensure the obj is for the current form
var input = vs.findElementByName(element, obj.$name);
if (input.length > 0) {
func(input, obj);
}
}
});
}
}
scope.$watch(function () { return form.$error; }, function (newval, oldval, scp) {
iterateOverErrorsObject(oldval, function (input, obj) {
vs.hideErrors(input);
}, true);
iterateOverErrorsObject(newval, function (input, obj) {
vs.showErrors(input, obj, form._attr);
});
}, true);
//something told the validator to show it's errors
scope.$on('show-errors', function (evt) {
iterateOverErrorsObject(form.$error, function (input, obj) {
vs.showErrors(input, obj, form._attr);
});
});
scope.$on('hide-errors', function (evt) {
vs.hideAllErrors(form);
});
}
};
}])
//this directive is to be put on the ng-form element and will dynamically add/remove validators based on the validations configuration
//which comes back from the service call "Validate"
.directive('dynamicValidators', ['$compile', function ($compile) {
return {
priority: 0,
restrict: 'A',
//require: 'ngModel',
require: '^form',
scope: {
'validations': '=',
},
link: function (scope, element, attrs, ctrl) {
(function (form, scp) {
// this will hold any information necessary to get the error message displayed
// **have to add the form because the ctrl gets recreated every time the form.$error changes
function setAttr(ctrl, key, value) {
if (!ctrl._attr)
ctrl._attr = {};
if (!form._attr)
from._attr = {};
ctrl._attr[key] = value;
var obj = form._attr[ctrl.$name] = {};
obj[key] = value;
};
scope.$watch('validations', function (nv, ov) {
form._attr = {};
//remove old validators
if (ov && ov.length > 0) {
_.each(ov, function (e) {
var fctrl = form[e.MemberNames[0]];
if (fctrl && fctrl.$validators) {
delete fctrl.$validators[e.ErrorKey];
//fctrl.$setValidity(e.ErrorKey, true);
fctrl.$validate();
}
});
}
//add new validators
if (nv && nv.length > 0) {
_.each(nv, function (e) {
var fctrl = form[e.MemberNames[0]];
if (!fctrl)
return;
if (e.ErrorKey == 'required') {
setAttr(fctrl, e.ErrorKey, e.ErrorValue);
fctrl.$validators[e.ErrorKey] = function (modelValue, viewValue) {
if (modelValue instanceof Array)
return modelValue.length > 0;
else
return modelValue !== '' && modelValue !== null && modelValue !== undefined;
};
} else if (e.ErrorKey == 'alphanumeric') {
setAttr(fctrl, e.ErrorKey, e.ErrorValue);
fctrl.$validators[e.ErrorKey] = function (modelValue, viewValue) {
return viewValue == null || (viewValue != null && /^[a-zA-Z0-9]*$/.test(modelValue));
};
} else if (e.ErrorKey == 'min') {
setAttr(fctrl, e.ErrorKey, e.ErrorValue);
fctrl.$validators[e.ErrorKey] = function (modelValue, viewValue) {
return modelValue === undefined || modelValue === null || modelValue === "" || modelValue >= e.ErrorValue;
}
} else if (e.ErrorKey == 'max') {
setAttr(fctrl, e.ErrorKey, e.ErrorValue);
fctrl.$validators[e.ErrorKey] = function (modelValue, viewValue) {
return modelValue === undefined || modelValue === null || modelValue === "" || modelValue <= e.ErrorValue;
}
}
//make the validator fire to set the status of the validator
if (fctrl.$validators[e.ErrorKey])
//fctrl.$setValidity(e.ErrorKey, fctrl.$validators[e.ErrorKey](fctrl.$modelValue, fctrl.$viewValue))
fctrl.$validate();
});
}
});
})(ctrl, scope);
},
}
}]);

If you still want to send to the server invalid data, you can use the allowInvalid option with the ngModelOptions directive:
<input type="text" name="userName"
ng-model="user.name"
ng-model-options="{ allowInvalid: true }" />
From the documentation for ngModelOptions:
Model updates and validation
The default behaviour in ngModel is that the model value is set to
undefined when the validation determines that the value is invalid. By
setting the allowInvalid property to true, the model will still be
updated even if the value is invalid.

Related

Custom directive don't want scope in this how can i achieve?

function contentValidator() {
var _matchContent = {
require: 'ngModel',
scope: {
contentValidator: '='
},
link: contentValidatorFn
};
return _matchContent;
function contentValidatorFn(scope, element, attrs, ctrl) {
scope.$watch(function() {
var combined;
if (scope.contentValidator || ctrl.$viewValue) {
combined = scope.contentValidator + '_' + ctrl.$viewValue;
}
return combined;
}, function(value) {
if (value) {
var origin = scope.contentValidator;
if (origin !== ctrl.$viewValue) {
ctrl.$setValidity("contentValidator", false);
return undefined;
} else {
ctrl.$setValidity("contentValidator", true);
return ctrl.$viewValue;
}
}
});
}
}
I'd suggest you do use $validators pipeline to set validity of field of form.
ngModel.$validators.contentValidator = function(modelValue, viewValue) {
var value = modelValue || viewValue;
return condition ? value : undefined; //condition would be what you wanted to check
};
Basically when you return defined value from $validators contentValidator function, but when you don't return angular will add content-validator class on that fields & the same property gets added to that form field like myForm.formFields.$error.contentValidator = true

How can I add an async validator to an input field?

In the documentation it shows this:
ngModel.$asyncValidators.uniqueUsername = function(modelValue, viewValue) {
var value = modelValue || viewValue;
// Lookup user by username
return $http.get('/api/users/' + value).
then(function resolved() {
//username exists, this means validation fails
return $q.reject('exists');
}, function rejected() {
//username does not exist, therefore this validation passes
return true;
});
};
But how do I connect this with an input field? It doesn't give an example of this.
You'd apply your validator inside of a directive link function. The directive must require ngModel, and then you'd apply that directive as an attribute to the input tag. In your case, that might look a bit like this:
angular.directive('isUniqueName', function () {
return {
require: 'ngModel',
link: function ($scope, $elem, $attrs, ngModel) {
ngModel.$asyncValidators.uniqueUsername = function(modelValue, viewValue) {
var value = modelValue || viewValue;
// Lookup user by username
return $http.get('/api/users/' + value).
then(function resolved() {
//username exists, this means validation fails
return $q.reject('exists');
}, function rejected() {
//username does not exist, therefore this validation passes
return true;
})
}
}
}
})
And then in your HTML code:
<input type='text' name='username' ng-model='username' is-unique-name />

How to access an attribute in the directive validator in AngularJS correctly

I'm making a validator which validates valid dates like MM/YYYY, but I didn't get how to access an attribute when the model changes:
<input id="my-date"
validate-short-date
data-max-date="{{thisMonth}}"
type="text"
name="myDate"
data-ng-model="myModelDate">
Here is the directive
.directive('validateShortDate', ['moment', function(moment) {
return {
restrict: 'A',
require: 'ngModel',
link: function($scope, element, attr, ngModel) {
var maxDate = false;
var pattern, regex;
pattern = '^((0[0-9])|(1[0-2])|[1-9])\/(19|20)[0-9]{2}$';
regex = new RegExp(pattern, 'i');
if(!angular.isUndefined(attr.maxDate)) {
// GOT ONLY ONCE
maxDate = attr.maxDate;
}
ngModel.$validators.maxDate = function(modelValue) {
// maxDate var is undefined after the first time
if (maxDate && regex.test(modelValue)) {
var modelDate = moment(modelValue, 'MM/YYYY').format('YYYYMM');
return modelDate <= maxDate;
}
return true;
};
ngModel.$validators.valid = function(modelValue) {
return modelValue === '' || modelValue === null || angular.isUndefined(modelValue) || regex.test(modelValue);
};
}
};
}])
The validator ngModel.$validators.valid works perfect, but inside ngModel.$validators.maxDate i cannot get the attr.maxDate but the first time directive fires.
So how can I access to a custom attribute value every time I check the modelValue?
I'm not an expert with AngularJS and probably I'm missing something important.
The attrs argument in the link function provides you with a $observe method which you can use to attach a listener function for dynamic changes in an attribute value.
It is very simple to use inside of your link function:
attr.$observe('maxDate', function() {
scope.maxDate = attr.maxDate;
ngModel.$validate();
});
Here is a working Plunker
You can do like this for track the change in ng-model:-
HTML
<input id="my-date"
validate-short-date
data-max-date="{{thisMonth}}"
type="text"
name="myDate"
data-ng-model="myModelDate">
Angularjs code:-
app.directive('validateShortDate', ['moment', function(moment) {
return {
restrict: 'A',
require: 'ngModel',
link: function($scope, element, attr, ngModel) {
var maxDate = false;
var pattern, regex;
pattern = '^((0[0-9])|(1[0-2])|[1-9])\/(19|20)[0-9]{2}$';
regex = new RegExp(pattern, 'i');
if(!angular.isUndefined(attr.maxDate)) {
// GOT ONLY ONCE
maxDate = attr.maxDate;
}
ngModel.$validators.maxDate = function(modelValue) {
// maxDate var is undefined after the first time
if (maxDate && regex.test(modelValue)) {
var modelDate = moment(modelValue, 'MM/YYYY').format('YYYYMM');
return modelDate <= maxDate;
}
return true;
};
ngModel.$validators.valid = function(modelValue) {
return modelValue === '' || modelValue === null || angular.isUndefined(modelValue) || regex.test(modelValue);
};
}
$scope.$watch('ngModel',function(){
console.log(attr.dataMaxDate);
});
};
}])

min/max validations not working if values are changed later

i have requirement where min value of one field depends on the input given in another field.
<input type="number" name="minval" class="form-control" ng-model="user.minval"
ng-required="true">
this input is used to validate another field
<input type="number" name="inputval" class="form-control" ng-model="user.inputval"
ng-required="true" min="{{user.minval}}">
but this is not working as expected.. if i change the "minval" later the input does not get revalidated..
i have tried setting the initial value for min from JS as was suggested in some solution but thats also not helping...
PLUNKER LINK
use ng-min/ng-max directives
app.directive('ngMin', function() {
return {
restrict: 'A',
require: 'ngModel',
link: function(scope, elem, attr, ctrl) {
scope.$watch(attr.ngMin, function(){
if (ctrl.$isDirty) ctrl.$setViewValue(ctrl.$viewValue);
});
var isEmpty = function (value) {
return angular.isUndefined(value) || value === "" || value === null;
}
var minValidator = function(value) {
var min = scope.$eval(attr.ngMin) || 0;
if (!isEmpty(value) && value < min) {
ctrl.$setValidity('ngMin', false);
return undefined;
} else {
ctrl.$setValidity('ngMin', true);
return value;
}
};
ctrl.$parsers.push(minValidator);
ctrl.$formatters.push(minValidator);
}
};
});
app.directive('ngMax', function() {
return {
restrict: 'A',
require: 'ngModel',
link: function(scope, elem, attr, ctrl) {
scope.$watch(attr.ngMax, function(){
if (ctrl.$isDirty) ctrl.$setViewValue(ctrl.$viewValue);
});
var maxValidator = function(value) {
var max = scope.$eval(attr.ngMax) || Infinity;
if (!isEmpty(value) && value > max) {
ctrl.$setValidity('ngMax', false);
return undefined;
} else {
ctrl.$setValidity('ngMax', true);
return value;
}
};
ctrl.$parsers.push(maxValidator);
ctrl.$formatters.push(maxValidator);
}
};
});
I've developed a couple of directives that actually restrict the user from setting an invalid value instead of simply throwing an error when an invalid value is provided.
These directives also do not require ngModel (though I doubt you would use them without) and what's really cool is that it will wrap the value around to the min/max if both settings are provided!
I've tried to simplify the directives as much as possible to make them easier for our readers.
Here is a JSFiddle of the whole thing: JSFiddle
And here are the directives:
app.directive('ngMin', function($parse){
return {
restrict: 'A',
link: function(scope, element, attrs){
function validate(){
if(element
&& element[0]
&& element[0].localName === 'input'
&& isNumber(attrs.ngMin)
&& isNumber(element[0].value)
&& parseFloat(element[0].value) < parseFloat(attrs.ngMin)){
if(isNumber(attrs.ngMax)){
element[0].value = parseFloat(attrs.ngMax);
if(attrs.hasOwnProperty("ngModel"))
$parse(attrs.ngModel).assign(scope, parseFloat(attrs.ngMax));
}
else {
element[0].value = parseFloat(attrs.ngMin);
if(attrs.hasOwnProperty("ngModel"))
$parse(attrs.ngModel).assign(scope, parseFloat(attrs.ngMin));
}
}
}
scope.$watch(function(){
return attrs.ngMin + "-" + element[0].value;
}, function(newVal, oldVal){
if(newVal != oldVal)
validate();
});
validate();
}
};
});
app.directive('ngMax', function($parse){
return {
restrict: 'A',
link: function(scope, element, attrs){
function validate(){
if(element
&& element[0]
&& element[0].localName === 'input'
&& isNumber(attrs.ngMax)
&& isNumber(element[0].value)
&& parseFloat(element[0].value) > parseFloat(attrs.ngMax)){
if(isNumber(attrs.ngMin)){
element[0].value = parseFloat(attrs.ngMin);
if(attrs.hasOwnProperty("ngModel"))
$parse(attrs.ngModel).assign(scope, parseFloat(attrs.ngMin));
}
else {
element[0].value = parseFloat(attrs.ngMax);
if(attrs.hasOwnProperty("ngModel"))
$parse(attrs.ngModel).assign(scope, parseFloat(attrs.ngMax));
}
}
}
scope.$watch(function(){
return attrs.ngMax + "-" + element[0].value;
}, function(newVal, oldVal){
if(newVal != oldVal)
validate();
});
validate();
}
};
});
...also, you will need this little helper function as well:
function isNumber(n){
return !isNaN(parseFloat(n)) && isFinite(n);
}
To invoke these directives, just set them on an input box where type="number":
<input ng-model="myModel" ng-min="0" ng-max="1024" />
And that should do it!
When you provide both an ngMin and ngMax, these directive will wrap the value around, so that when your value becomes less than ngMin, it will be set to ngMax, and vice-versa.
If you only provide ngMin or ngMax, the input value will simply be capped at these values.
I prefer this method of preventing bad values rather than alerting the user that they have entered a bad value.

angular directive scope[attrs.ngModel] is not working

if the user input is not a number, i have to revert to old number value.
setting scope value from directive is not working.
http://jsfiddle.net/vfsHX/149/
app.directive('isNumber', function () {
return {
require: 'ngModel',
link: function (scope, element, attrs) {
scope.$watch(attrs.ngModel, function(newValue,oldValue) {
var arr = String(newValue).split("");
if (arr.length === 0) return;
if (arr.length === 1 && (arr[0] == '-' || arr[0] === '.' )) return;
if (arr.length === 2 && newValue === '-.') return;
if (isNaN(newValue)) {
console.log(oldValue);
scope[attrs.ngModel] = oldValue;
}
});
}
};
});
use of $setViewValue solves the issue
http://jsfiddle.net/vfsHX/158/
if(isNaN(newValue))
{
ngModel.$setViewValue(oldValue);
ngModel.$render();
}
Your model is in nested form hence when you try to access using scope[attrs.ngModel], you are referening to model which is not there. Instead of using the nested javascript model if you directly give a reference then its working. Check out the fiddle here http://jsfiddle.net/ztUsc/1/

Resources