$watch does not watch model when using scope attribute - angularjs

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

Related

$observe not firing on changes and not interpolating

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.

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

Difference between modelValue and viewValue

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)

undefined scope into $watch

I have a directive named dir with:
ng-model="job.start_date"
comparison-date="job.end_date
Into scope.$watch("comparisonDate... I want to access my ng-model value. The problem is that scope is undefined into watch's callback function. The Question is: How can I get the ng-value inside this function?
.directive("dir", function() {
return {
scope: {
comparisonDate: "=",
ngModel: "="
},
link: function (scope, element, attrs, ctrl) {
var foo = scope.ngModel;
scope.$watch("comparisonDate", function(value, oldValue) {
console.log(value); //comparisonDate showing value properly
console.log(scope.ngModel); //Undefined
console.log(foo) //shows value but it's not refreshing. It shows allways the initial value
})
}
};
})
the view...
<input dir type="text" ng-model="job.start_date" comparison-date="job.end_date"/>
During the linking phase of the directive, the value may not be available. You can use $observe to observe the value change.
attrs.$observe("comparisonDate", function(a) {
console.log(scope.ngModel);
})
ng-model is built-in directive that tells Angular to do two-way data binding. http://docs.angularjs.org/api/ng.directive:ngModel
It looks like you are using the value of properties of the same object job to do comparison. If you want to stick with ng-model, you can use NgModelController: http://docs.angularjs.org/api/ng.directive:ngModel.NgModelController
Then change the view to:
<input dir type="text" ng-model="job"/>
and change the directive to:
.directive("dir", function() {
return {
require: '?ngModel', // get a hold of NgModelController
link: function (scope, element, attrs, ngModel) {
// access the job object
ngModel.$formatters.push(function(job){
console.log(job.start_date);
console.log(job.end_date);
});
}
};
})
Or you can change the attribute name from ng-model to some words haven't reserved. For example change the view like:
<input dir type="text" comparison-start-date="job.start_date" comparison-end-date="job.end_date"/>
Try scope.$watch(attrs.comparisonDate, ...) and then use attrs.ngModel

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