With a custom directive, a validation function is added to validate integer input
var INTEGER_REGEXP = /^\-?\d+$/;
app.directive('integer', function() {
return {
require: 'ngModel',
link: function(scope, elm, attrs, ctrl) {
ctrl.$validators.integer = function(modelValue, viewValue) {
if (ctrl.$isEmpty(modelValue)) {
// consider empty models to be valid
return true;
}
if (INTEGER_REGEXP.test(viewValue)) {
// it is valid
return true;
}
// it is invalid
return false;
};
}
};
});
Each function in the $validators object receives the modelValue and the viewValue.
What is the difference between modelValue and viewValue ?
It is possible to define $formatters and $parsers in your ngModelController. The viewValue is the value that the rendering directive uses to draw itself, the modelValue is what is stored in the scope once ngModel's $parser list hase been applied. If you change the value in the scope, ngModel will run that value through its $formatters which is then read by the rendering directive as a viewValue.
Often the viewValue is the string that is displayed in an input element whereas the modelValue is the value that has been parsed into the target format (a Date object in a datepicker directive, for example)
Related
TLDR: Why does angular's ngMinlength receive $observe updates with interpolated values, but my custom validation directive does not?
Link to plnkr
I am working on a custom validation directive in Angular 1.3 and have noticed something that seems inconsistent. The directive in angular seems to get interpolated updates from attr.$observe, but the directive I create does not behave the same way.
I can use $watch to fix it, or bind an interpolated value, but is inconsistent with the existing validation directives. What's the difference, and how can I make my custom directive work similarly to the built in validation directives?
Angular's directive
var minlengthDirective = function() {
return {
restrict: 'A',
require: '?ngModel',
link: function(scope, elm, attr, ctrl) {
if (!ctrl) return;
var minlength = 0;
attr.$observe('minlength', function(value) {
minlength = int(value) || 0;
ctrl.$validate();
});
ctrl.$validators.minlength = function(modelValue, viewValue) {
return ctrl.$isEmpty(viewValue) || viewValue.length >= minlength;
};
}
};
};
My directive
function observeMinLength($log){
return {
restrict: 'A',
require: '?ngModel',
link: function (scope, elm, attr, ctrl) {
if (!ctrl) return;
var min;
//Problem 1: observered value is not interpolated
//Problem 2: observe is only fired one time
attr.$observe('observeMinlength', function (value) {
$log.debug('observed value: ' + value);
min = parseInt(value, 10) || 0;
ctrl.$validate();
});
ctrl.$validators.mymin = function (modelValue, viewValue) {
var len = 0;
if (viewValue){
len = viewValue.length;
}
return ctrl.$isEmpty(viewValue) || viewValue.length >= min;
};
}
};
}
Notice that the ngMinlength directive is $observing the "minlength" attribute, not the "ngMinlength" attribute. I believe the issue is that Angular's input directive is setting the minlength attribute based on the interpolated value... so while the ngMinlength attribute doesn't change as its value changes, the minlength attribute does which is why it is observable. In your directive, the attribute observeMinLength doesn't change, but the value of the scope property you pass in does.
If you look in the angular source code: https://github.com/angular/angular.js/blob/13b7bf0bb5262400a06de6419312fe3010f79cb2/src/ng/directive/attrs.js#L379 you can see that angular is watching the scope variable and setting the attribute variable for all attributes in the ALIASED_ATTR collection. That collection is defined as
var ALIASED_ATTR = {
'ngMinlength': 'minlength',
'ngMaxlength': 'maxlength',
'ngMin': 'min',
'ngMax': 'max',
'ngPattern': 'pattern'
};
in https://github.com/angular/angular.js/blob/2e0e77ee80236a841085a599c800bd2c9695475e/src/jqLite.js#L575
So in short, Angular has special handling for its own attribute based validation directives. So you will need to watch your scope property rather than observe the attribute.
I'm trying to write a custom directive to validate input value: does it belong to the specified range. The problem is that I can't access ng-model without knowing the name of the scope variable which is used for ng-model. Considering that directive has to be reused with different inputs I want to access ng-model directly. I did try to use scope[attrs.ngModel] but got the undefined value. How can read ng-model value inside directive? Thank you.
netupApp.directive('between', function() {
return {
restrict: 'A',
require: 'ngModel',
link: function(scope, element, attrs, ngModelCtrl) {
scope.$watch('dataSubmitting', function(dataSubmitting){
if (dataSubmitting) {
var min = Number(attrs.min);
var max = Number(attrs.max);
console.log(attrs.ngModel); // "number"
console.log(scope[attrs.ngModel]); // undefined
var inputText = scope.number; // that is the var used in ng-model
console.log(min); // 10
console.log(inputText); // would be the input value
console.log(max); //20
if (inputText <= min || inputText >= max) {
scope.alerts.push({
msg: 'error',
type: 'danger',
icon: 'warning',
'closable': true
});
}
}
});
}
};
});
You should hook into the Angular validation system and add your validator function to either the $validators or $asyncValidators collections (in your case I think $validators is enough, no need for async).
The validator functions receive the model value as an argument :
link: function(scope, elm, attrs, ctrl) {
var min = Number(attrs.min);
var max = Number(attrs.max);
ctrl.$validators.between = function(modelValue, viewValue) {
if (modelValue <= min || modelValue >= max) {
//do something here or just return false
return false;
}
return true;
}
}
In the view you can get the validation error messages like this :
<div ng-messages="formName.inputName.$error">
<p ng-message="between">The value is not in the required range<p>
</div>
Reference doc : https://docs.angularjs.org/api/ng/type/ngModel.NgModelController
The proper way to get the ngModel.$viewValue is:
app.directive('between', function () {
return {
require: 'ngModel',
link: function (scope, element, attrs, ngModel) {
ngModel.$render = function () {
var newValue = ngModel.$viewValue;
console.log(newValue)
};
}
};
});
Have a look at tutorial underneath when wanting to invoke the ngModel.$setViewVAlue from the directive
https://egghead.io/lessons/angularjs-using-ngmodel-in-custom-directives
According to angluarJs doc:$parse
you need to parse the attrs, please try:
var getter=$parse(attrs.ngModel);
var setter=getter.assign;
setter(scope,getter(scope));
I'm working on a directive made for <input> that watches what the user types. When the user types in a valid phone number it should re-format the input model object and set a variable to true, indicating to the parent scope that the model now contains a valid phone number.
CountryISO is depency injected as constant and contains - surprise - the country code of the user. The functions used inside the directive are from phoneformat.js
UPDATED: Now the valid = true/false assigment works. But how do I then update the actual model value? This needs to be changed to the properly formatted phone number.
app.directive('phoneFormat', ['CountryISO', function (CountryISO) {
return {
restrict: 'A',
require: 'ngModel',
scope: {
valid:'='
},
link: function (scope, element, attrs, ngModel) {
scope.$watch(function() {
return ngModel.$viewValue; //will return updated ng-model value
}, function(v) {
if (isValidNumber(v, CountryISO)) {
// What do I do here? This doesn't work.
v = formatE164(CountryISO, v);
// This neither
ngModel.$viewValue = formatE164(CountryISO, v);
console.log("valid");
scope.valid = true;
} else {
console.log("invalid");
scope.valid = false;
}
});
}
};
}]);
as the directive
and the html looks like:
<input data-phone-format data-valid="user.validPhoneNumber" data-ng-model="user.info.ph">
The problem is that as soon as I include scope: {valid:'='}, as part of the directive, the $watch stops working. How do I get both? I want the directive to be able to point to a variable in the parent scope that should change from true to false depending on the validity of the phone number.
Because as you declaring watcher the variable are parsing with the directive scope which becomes isolated scope after you have added scope: {valid:'='} to make it working you could place watch on the ngModel.$viewValue
To updated the ngModel value you could set $viewValue of ngModel using $setViewValue method of ngModel & then to update that value inside $modelValue you need to do $render() on ngModel.
Read on $viewValue & $modelValue
Link
link: function(scope, element, attrs, ngModel) {
scope.$watch(function() {
return ngModel.$viewValue; //will return updated ng-model value
}, function(v) {
if (isValidNumber(v, CountryISO)) {
// What do I do here? This doesn't work.
v = formatE164(CountryISO, v);
// This neither
ngModel.$setViewValue(formatE164(CountryISO, v));
ngModel.$render(); //to updated the $modelValue of ngModel
console.log("valid");
scope.valid = true;
} else {
console.log("invalid");
scope.valid = false;
}
});
}
I created a stringToDate directive. The element is wrapped in an ng-repeat directive with a filter. As the filter changes, the element with the directive appears and disappears. When debugging the code, the ngModel contains a $viewValue of NaN, and the $modelValue is undefined. Thus after flipping the filter a few times on the ng-repeat the value is empty. Why is the $parser not called? Am I not handling this correctly? The Date function is from DateJS.
.directive('stringToDate', function() {
return {
require: 'ngModel',
link: function(scope, element, attrs, ngModel) {
ngModel.$parsers.push(function(value) {
return '' + value;
});
ngModel.$formatters.push(function(value) {
var a = ngModel;
if(Boolean(value))
return Date.parse(value).toString('MM/dd/yyyy');
});
}
};
})
I'm not sure this is the proper solution, but I added the following to the directive to clean up the parsers. I also call parseAndValidate to return the $viewValue to the $modelValue.
element.on('$destroy', function(){
ngModel.$$parseAndValidate();
ngModel.$formatters.pop();
ngModel.$parsers.pop();
});
I'm writing a custom directive to validate some value in the scope. It should work like the required attribute, but instead of validating the input text, it's going to validate a value in the scope. My problem is that this value is set in a $scope.$watch function and this function runs after my directive. So when my directive tries to validate the value it has not been set yet. Is it possible to run the $watch code before running my custom directive?
Here is the code:
var app = angular.module('angularjs-starter', []);
app.controller('MainCtrl', function($scope) {
var keys = {
a: {},
b: {}
};
$scope.data = {};
// I need to execute this before the directive below
$scope.$watch('data.objectId', function(newValue) {
$scope.data.object = keys[newValue];
});
});
app.directive('requiredAttribute', function (){
return {
require: 'ngModel',
link: function(scope, elem, attr, ngModel) {
var requiredAttribute = attr.requiredAttribute;
ngModel.$parsers.unshift(function (value) {
ngModel.$setValidity('requiredAttribute', scope[attr.requiredAttribute] != null);
return value;
});
}
};
});
<input type="text" name="objectId" ng-model="data.objectId" required-attribute="object" />
<span class="invalid" ng-show="myForm.objectId.$error.requiredAttribute">Key "{{data.objectId}}" not found</span>
And here is a plunker: http://plnkr.co/edit/S2NrYj2AbxPqDrl5C8kQ?p=preview
Thanks.
You can schedule the $watch to happen before the directive link function directly. You need to change your link function.
link: function(scope, elem, attr, ngModel) {
var unwatch = scope.$watch(attr.requiredAttribute, function(requiredAttrValue) {
if (requiredAttribute=== undefined) return;
unwatch();
ngModel.$parsers.unshift(function (value) {
ngModel.$setValidity('requiredAttribute', requiredAttrValue != null);
return value;
});
});
}
This approach will activate the $watch function inside the directive only once and will remove the watcher the first time your required scope variable is set.
There is also another approach where you parse the value and check it this way:
link: function(scope, elem, attr, ngModel) {
var parsedAttr = $parse(attr.requiredAttribute);
ngModel.$parsers.unshift(function (value) {
ngModel.$setValidity('requiredAttribute', parsedAttr(scope) != null);
return value;
});
}
Here you will need to use $parse AngularJS service. The difference here is that this will mark the input field as invalid without waiting for first value set on the required scope variable.
Both variants allow you to pass an expression instead of a simple variable name. This makes it possible to write something as required-attribute="object.var1.var2".
It really depends on what you need.