Custom validation not triggered when setting model value programmatically - angularjs

Created a custom validation directive that invalidates an input when anything else than a number or space is input. When I change the value programmatically to something that should pass validation, the validation state is not changed.
Check this JSFIDDLE and see for yourself. Any ideas?
<div ng-app="test" ng-controller="Ctrl">
<form name="form">
<input custom-validation type="text" ng-model="box.text" name="text" />
</form>
<button ng-click="change()">Change to numbers only</button>
Why doesn't changing to numbers only pass the validation?
</div>
angular.module('test', []);
angular.module('test').directive('customValidation', function () {
'use strict';
return {
require: '?ngModel',
link: function (scope, element, attrs, ngModelCtrl) {
ngModelCtrl.$parsers.push(function removeIllegalCharacters(viewValue) {
if(viewValue === undefined){
ngModelCtrl.$setValidity('numbersAndSpacesOnly', true); //empty value is always valid
} else {
var clean = viewValue.replace(/[^0-9 ]+/g, '');
if (viewValue === clean) {
ngModelCtrl.$setValidity('numbersAndSpacesOnly', true);
} else {
ngModelCtrl.$setValidity('numbersAndSpacesOnly', false);
}
}
return viewValue;
});
}
};
});
angular.module('test').controller('Ctrl', function ($scope, $timeout) {
$scope.change = function () {
$scope.box.text = '12345';
}
});

ngModel uses 2 pipelines (arrays) of code for validation:
The $parsers array has functions to apply to the view value when it changes from the user; each function is called with the value returned form the previous, the first function is called with the view value and the return of the last function is written in the model. This is commonly used to validate and convert user input (e.g. from the text of an input type="text" to a number).
The $formatters array works similarly, but in the opposite direction. It receives the model value and transforms it, with the return of the last function being the new view value.
Functions in both pipelines may choose to call ngModel.$setValidity() to alter the validity state of the ngModel.
For the scope of this question: in order to validate the model value, you have to use the $formatters similarly to the $parsers you are already using:
angular.module('test').directive('customValidation', function () {
'use strict';
return {
require: '?ngModel',
link: function (scope, element, attrs, ngModelCtrl) {
function removeIllegalCharacters(value) {
if(value === undefined){
ngModelCtrl.$setValidity('numbersAndSpacesOnly', true); //empty value is always valid
} else {
var clean = value.replace(/[^0-9 ]+/g, '');
if (value === clean) {
ngModelCtrl.$setValidity('numbersAndSpacesOnly', true);
} else {
ngModelCtrl.$setValidity('numbersAndSpacesOnly', false);
}
}
return value;
}
ngModelCtrl.$parsers.push(removeIllegalCharacters);
ngModelCtrl.$formatters.push(removeIllegalCharacters);
}
};
});

Related

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 do I attach validators on an input field in angular that accesses another model in the form

I have a range input (between [lowerValue] and [upperValue]) on a form and I want to make a reusable directive called 'validateGreaterThan' that can be attached to any form and use the ngModel $validators functionality so I can attach multiple ones onto an input.
You can check a simple demo on jsbin here:
http://jsbin.com/vidaqusaco/1/
I've set up a directive called nonNegativeInteger and that works correctly, however, the validateGreaterThan directive I have isn't working. How can I get it to reference the lowerValue?
I appreciate any help with this.
Here is the basic idea:-
Define 2 directives and let each directive refer to the other field buy passing its name. When the validator runs on the current field you can retrieve the model value of another field and now you have values from both fields and you can ensure the relation between the 2 fields.
As per my code below I have 2 fields minimumAmount and maximumAmount where the minimumAmount cannot be greater than the maximum amount and vice-versa.
<input name="minimumAmount" type="number" class="form-control"
ng-model="entity.minimumAmount"
less-than-other-field="maximumAmount" required/>
<input name="maximumAmount" type="number"
ng-model="entity.maximumAmount"
greater-than-other-field="minimumAmount"
class="form-control"/>
Here we have 2 directives lessThanOtherField and greaterThanOtherField and they both refer to other field as we pass the other field name. greater-than-other-field="minimumAmount" we are passing the other field.
.directive('lessThanOtherField', ['$timeout',function($timeout){
return {
require: 'ngModel',
link: function (scope, elm, attrs, ctrl) {
var xFieldValidatorName = 'lessThanOtherField';
var form = elm.parent().controller('form');
var otherFieldName = attrs[xFieldValidatorName];
var formFieldWatcher = scope.$watch(function(){
return form[otherFieldName];
}, function(){
formFieldWatcher();//destroy watcher
var otherFormField = form[otherFieldName];
var validatorFn = function (modelValue, viewValue) {
var otherFieldValue = otherFormField.hasOwnProperty('$viewValue') ? otherFormField.$viewValue : undefined;
if (angular.isUndefined(otherFieldValue)||otherFieldValue==="") {
return true;
}
if (+viewValue < +otherFieldValue) {
if (!otherFormField.$valid) {//trigger validity of other field
$timeout(function(){
otherFormField.$validate();
},100);//avoid infinite loop
}
return true;
} else {
// it is invalid, return undefined (no model update)
//ctrl.$setValidity('lessThanOtherField', false);
return false;
}
};
ctrl.$validators[xFieldValidatorName] = validatorFn;
});
}
};
}])
.directive('greaterThanOtherField', ['$timeout',function($timeout){
return {
require: 'ngModel',
link: function (scope, elm, attrs, ctrl) {
var xFieldValidatorName = 'greaterThanOtherField';
var form = elm.parent().controller('form');
var otherFieldName = attrs[xFieldValidatorName];
var formFieldWatcher = scope.$watch(function(){
return form[otherFieldName];
}, function(){
formFieldWatcher();//destroy watcher
var otherFormField = form[otherFieldName];
var validatorFn = function (modelValue, viewValue) {
var otherFieldValue = otherFormField.hasOwnProperty('$viewValue') ? otherFormField.$viewValue : undefined;
if (angular.isUndefined(otherFieldValue)||otherFieldValue==="") {
return true;
}
if (+viewValue > +otherFieldValue) {
if (!otherFormField.$valid) {//trigger validity of other field
$timeout(function(){
otherFormField.$validate();
},100);//avoid infinite loop
}
return true;
} else {
// it is invalid, return undefined (no model update)
//ctrl.$setValidity('lessThanOtherField', false);
return false;
}
};
ctrl.$validators[xFieldValidatorName] = validatorFn;
});
}
};
}])

How to force date input bind to model as JavaScript date Date()

I'm trying to get input from text boxes to bind to scope variables as actual JavaScript Date objects, not strings. The inputs are generated dynamically so I'm unable to cast/convert before the values are sent to the server.
So far, I have created a directive that uses moment.js to parse the value on the scope, and convert it to a Date() object. The problem seems to be that angular converts the value back to a string immediately after. I guess it rechecks the html input and overwrites the Date() object set in the directive.
Here is a working Plunkr demonstrating the issue
(function () {
'use strict';
angular.module('app', ['ng'])
.controller('myController', ['$scope', function() {
$scope.testObj = null;
}])
.directive('dateBinding', function () {
return {
restrict: 'A',
require: 'ngModel',
scope: false,
link: function (scope, element, attrs, ngModel) {
var parseFormat = attrs.dateBinding;
scope.$watch(
function() {
console.log('watching model', ngModel.$modelValue);
return ngModel.$modelValue;
},
function (val) {
console.log('recieved model', val);
if (val && typeof val == 'string') {
console.log('attempting parse date', val);
if(moment(val, parseFormat).isValid())
{
console.log('string is valid date');
ngModel.$modelValue = moment(val, parseFormat).toDate();
console.log('completed value assignment', ngModel.$modelValue);
console.log('model is of type ' + typeof ngModel.$modelValue);
console.log('model is date', (ngModel.$modelValue instanceof Date));
}
else
{
console.log('string is not a valid date');
}
}
}
);
}
};
})
} ());
You can see the behaviour by opening the console in a browser while running the plunkr. The line 'completed value assignment' shows that at least momentarily, ngModel.$modelValue (from $scope.testObj) is a Date() object.
The final line in the output below shows the watch firing again, and the model value is once again a string as it appears in the html input.
How can I have the value persist as a Date object (once a valid date can be parsed).
You have to use the $parsers and $formatters pipelines, described in the docs of ngModelController. The code would be:
.directive('dateBinding', function () {
return {
restrict: 'A',
require: 'ngModel',
scope: false,
link: function (scope, element, attrs, ngModel) {
var parseFormat = attrs.dateBinding;
function parse(value) {
var m = moment(value, parseFormat);
if( m && m.isValid() ) {
ngModel.$setValidity('dateBinding', true);
return m.toDate();
}
else {
ngModel.$setValidity('dateBinding', false);
return; // undefined
}
}
function format(value) {
if( value && value instanceof Date ) {
return moment(d).format(parseFormat);
}
else {
return '';
}
}
ngModel.$formatters.push(format);
ngModel.$parsers.unshift(parse);
}
};
});
See (and play with) the forked plunk: http://plnkr.co/edit/VboH2iq6HRlaDhX3g1AY?p=preview

can anybody explain this directive?

Am new to angularjs. I have seen this piece of code from this link. It is working fine for me. But i am not getting how it is working ? From where it was called?
Can anybody explain this ?
var app = angular.module('plunker', []);
var ValidSubmit = ['$parse', function ($parse) {
return {
compile: function compile(tElement, tAttrs, transclude) {
return {
post: function postLink(scope, element, iAttrs, controller) {
var form = element.controller('form');
form.$submitted = false;
var fn = $parse(iAttrs.validSubmit);
element.on('submit', function(event) {
scope.$apply(function() {
element.addClass('ng-submitted');
form.$submitted = true;
if(form.$valid) {
fn(scope, {$event:event});
}
});
});
scope.$watch(function() { return form.$valid}, function(isValid) {
if(form.$submitted == false) return;
if(isValid) {
element.removeClass('has-error').addClass('has-success');
} else {
element.removeClass('has-success');
element.addClass('has-error');
}
});
}
}
}
}
}]
app.directive('validSubmit', ValidSubmit);
Here is the gist of what the directive is doing:
A function is assigned to handle the "onsubmit" event for that element
It looks like the directive is intended to be an attribute, which accepts a function as its value. That function is retrieved:
var fn = $parse(iAttrs.validSubmit);
An event handler for onsubmit is set up:
element.on('submit', function(event) {
And then that function is called if the form is valid:
if(form.$valid) {
fn(scope, {$event:event});
The directive monitors the state of the form that contains the element (valid or invalid)
The $watch function is used to fire an event whenever the form changes between valid and invalid:
scope.$watch(function() { return form.$valid}
Whenever the form is submitted, CSS classes are applied to the element
When the validity of the form changes, the has-error or has-success CSS class is applied to the element (based on whether the form is valid or not).
Note that the CSS classes will only be added / removed if the form is being submitted, due to the guard clause at the beginning of that function:
if(form.$submitted == false) return;

angularjs directive validation unique email check before send

html
<input type="email" data-input-feedback="" data-ng-model="user.email" data-unique-email="" required="required" placeholder="Email" class="form-control" id="email" name="email">
js
.directive('uniqueEmail',function (User) {
return {
require:'ngModel',
restrict:'A',
link:function (scope, element, attrs, ngModelCtrl) {
ngModelCtrl.$parsers.push(function (viewValue) {
/*
Is there a way to check if it's a valid email ?
both ngModelCtrl.$valid and ngModelCtrl.$error.email doesn't work
*/
User.isUniqueEmail(viewValue).then(function(data){
ngModelCtrl.$setValidity('uniqueEmail', !data.email);
});
return viewValue;
});
}
};
});
so is there a way to check if it's a valid email
before sending the value to the server ?
UPDATE
ngModelCtrl.$parsers.push
using push() here to run it as the last parser, after we are sure that other validators were run
so only if required and email validation are passed
do the call to check for unique email
END UP
.directive('uniqueEmail',function (User) {
return {
require:'ngModel',
restrict:'A',
controller:function ($scope) {
$scope.isValidEmail = function(){
return $scope.form.email.$error.email;
}
},
link:function (scope, element, attrs, ngModelCtrl) {
var original;
// If the model changes, store this since we assume it is the current value of the user's email
// and we don't want to check the server if the user re-enters their original email
ngModelCtrl.$formatters.unshift(function(modelValue) {
original = modelValue;
return modelValue;
});
// using push() here to run it as the last parser, after we are sure that other validators were run
ngModelCtrl.$parsers.push(function (viewValue) {
if (viewValue && viewValue !== original ) {
if(scope.isValidEmail(viewValue)){
User.isUniqueEmail(viewValue).then(function(data){
ngModelCtrl.$setValidity('uniqueEmail', !data.email);
});
}
return viewValue;
}
});
}
};
});
With compile and priority
.directive('uniqueEmail',function (User) {
return {
require:'ngModel',
restrict:'A',
priority:0,
compile: function compile(tElement, tAttrs, transclude) {
return function (scope, element, attrs, ngModelCtrl) {
var original;
// If the model changes, store this since we assume it is the current value of the user's email
// and we don't want to check the server if the user re-enters their original email
ngModelCtrl.$formatters.unshift(function(modelValue) {
original = modelValue;
ngModelCtrl.$setViewValue(original);
return modelValue;
});
// using push() here to run it as the last parser, after we are sure that other validators were run
ngModelCtrl.$parsers.push(function (viewValue) {
if (viewValue && viewValue !== original ) {
if(scope.isValidEmail()){
User.isUniqueEmail(viewValue).then(function(data){
ngModelCtrl.$setValidity('uniqueEmail', !data.email);
});
}
return viewValue;
}
});
scope.isValidEmail = function(){
return scope.form.email.$isvalid;
}
}
}
}
});
it still doesnt work the value of scope.form.email.$isvalid
is unreiable and seems out of date :(
The order of the parsers is important. I am not sure but your parser may have got registered before the inbuilt parsers and hence gets fired first.
Maybe if you splice the $parsers array and insert your parser at 0 it may work.

Resources