$observe not firing on changes and not interpolating - angularjs

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.

Related

AngularJS Custom Validation Directives - How to avoid using isolated scope

I'm attempting to use multiple angular validators on a text field, but have run into the Multiple directives requesting isolated scope error. (Please read on before closing as a duplicate.)
All the solutions I've seen so far, recommend removing the scope: {...} from the offending directives, however for my scenario, I need to evaluate variables from the controller (and $watch them for changes).
I've tried using attrs.$observe, but I can't work out how to get the evaluated variables into the $validator function. (Additionally, I can't $observe the ngModel).
Please let me know if there's a another way to solve this issue.
Here's the smallest example I could put together. N.B. maxLength validator's scope is commented out, essentially disabling it:
angular
.module('app', [])
// validates the min length of a string...
.directive("minLen", function() {
return {
require: 'ngModel',
restrict: 'A',
scope: {
ngModel: '=',
minLen: '='
},
link: function(scope, element, attrs, ngModelCtrl) {
scope.$watch('ngModel', function(){
ngModelCtrl.$validate();
});
scope.$watch('minLen', function(){
ngModelCtrl.$validate();
});
ngModelCtrl.$validators.minLength = function(modelValue, viewValue) {
var value = modelValue || viewValue;
return angular.isUndefined(scope.minLen) ||
angular.isUndefined(value) ||
value.length >= scope.minLen;
};
}
};
})
.directive("maxLen", function() {
return {
require: 'ngModel',
restrict: 'A',
// Commented out for now - causes error.
// scope: {
// ngModel: '=',
// maxLen: "="
// },
link: function(scope, element, attrs, ngModelCtrl) {
scope.$watch('ngModel', function(){
ngModelCtrl.$validate();
});
scope.$watch('maxLen', function(){
ngModelCtrl.$validate();
});
ngModelCtrl.$validators.maxLength = function(modelValue, viewValue) {
var value = modelValue || viewValue;
return angular.isUndefined(scope.maxLen) ||
angular.isUndefined(value) ||
value.length >= scope.maxLen;
};
}
};
})
// this controller just initialises variables...
.controller('CustomController', function() {
var vm = this;
vm.toggleText = function(){
if (vm.text === 'aaa') {
vm.text = 'bbbbb';
} else {
vm.text = 'aaa';
}
}
vm.toggle = function(){
if (vm.minLen === 3) {
vm.minLen = 4;
vm.maxLen = 12;
} else {
vm.minLen = 3;
vm.maxLen = 10;
}
};
vm.toggleText();
vm.toggle();
return vm;
})
;
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.0/angular.min.js"></script>
<div ng-app="app" ng-controller="CustomController as ctrl">
<ng-form name="ctrl.form">
<label>Enter {{ctrl.minLen}}-{{ctrl.maxLen}} characters: </label>
<input
name="text"
type="text"
ng-model="ctrl.text"
min-len="ctrl.minLen"
max-len="ctrl.maxLen"
/>
</ng-form>
<br/><br/>
<button ng-click="ctrl.toggle()">Modify validation lengths</button>
<button ng-click="ctrl.toggleText()">Modify ngModel (text)</button>
<h3>Validation (just list $error for now)</h3>
<pre>{{ ctrl.form.text.$error | json }}</pre>
</div>
remove isolated scope
You dont need to observe, watch ngModel - when it changes, angular will run validator for you.
Decide how you want to use your directive: my-val-dir="{{valName}}" vs my-val-dir="valName". In first case you use attrs.$observe('myValDir'), in second $watch(attrs.myValDir) & $eval.
When your value gonna be something simple, like number or short string - first way seems good, when value is something big, i.e. array - use second approach.
Remove isolate scope and use scope.$watch to evaluate the attribute:
app.directive("minLen", function() {
return {
require: 'ngModel',
restrict: 'A',
scope: false,
// ngModel: '=',
// minLen: '='
// },
link: function(scope, element, attrs, ngModelCtrl) {
var minLen = 0;
scope.$watch(attrs.minLen, function(value){
minLen = toInt(value) || 0;
ngModelCtrl.$validate();
});
ngModelCtrl.$validators.minLength = function(modelValue, viewValue) {
return ngModelCtrl.$isEmpty(viewValue) || viewValue.length >= minLen;
};
}
};
})
There is no need to watch the ngModel attribute as the ngModelController will automatically invoke the functions in the $validators collection when the model changes.
When the watch expression is a string, the string attribute will be evaluated as an Angular Expression.
For more information, see AngularJS scope API Reference - $watch.

How to access ng-model from attribute directive?

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));

$watch does not watch model when using scope attribute

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;
}
});
}

$formatter leaves $modelValue empty

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();
});

Run $watch after custom directive

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.

Resources