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();
});
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 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 am using a directive to build a custom validator and it works fine. But, it was called only once! If my "roleItems" are updated, this directive was not called again! How can it be called every time when "roleItems" are updated?
Here are the markups. And "Not-empty" is my directive.
<form name="projectEditor">
<ul name="roles" ng-model="project.roleItems" not-empty>
<li ng-repeat="role in project.roleItems"><span>{{role.label}}</span> </li>
<span ng-show="projectEditor.roles.$error.notEmpty">At least one role!</span>
</ul>
</form>
This is my directive. It should check if the ng-model "roleItems" are empty.
angular.module("myApp", []).
directive('notEmpty', function () {
return {
require: 'ngModel',
link: function (scope, elm, attrs, ctrl) {
ctrl.$validators.notEmpty = function (modelValue, viewValue) {
if(!modelValue.length){
return false;
}
return true;
};
}
};
});
Main purpose of validator is validate ngModel value of user input or model change, so it should be uset to checkbox/textara/input and etc. You cant validate ng-model of everything. Angular is enough intelligent to knows that ng-model makes no sens so he is just ignoring it .
I you wanna change only error message you can check it via .length property. If you wanna make whole form invalid , i suggest you to make custom directive , put it on , and then in validator of this directive check scope.number.length > 0
Basically just adjust your directive code to input element and hide it .... via css or type=hidden, but dont make ngModel="value" its not make sense because ng-model is expecting value which can be binded and overwriteen but project.roleItems is not bindable! so put ng-model="dummyModel" and actual items to another param ...
<input type="hidden" ng-model="dummyIgnoredModel" number="project.roleItems" check-empty>
angular.module("myApp", []).
directive('checkEmpty', function () {
return {
require: 'ngModel',
link: function (scope, elm, attrs, ctrl) {
ctrl.$validators.push(function (modelValue, viewValue) {
if(!scope.number.length){
return false;
}
return true;
});
//now we must "touch" ngModel
scope.$watch(function()
{
return scope.number
}, function()
{
ctrl.$setViewValue(scope.number.length);
});
}
};
});
I am attempting to pass a test for my angularjs directive using jasmine.
My directive simply takes what is passed into the attribute (as a one way binding) and replaces the elements text with that value. My directive also watches for changes in the value and updates the elements text accordingly:
return {
restrict: 'A',
scope: {
test: '&'
},
link: function (scope, element, attrs) {
element.text(scope.test());
scope.$watch('test', function (value) {
console.log('WATCH FIRED!');
element.text(scope.test());
});
}
};
My jasmine test attempts to set the value passed into the directive, then change it, and then checks to see if the elements text is different after changing the value:
it('should update when the model updates', function () {
$scope.model = 'hello';
$scope.$digest();
var before = element.text();
$scope.model = 'world';
$scope.$digest();
expect(element.text()).not.toEqual(before);
});
But the test fails everytime. In fact, the elements text is always 'hello' and never gets changed to 'world'.
Plunker: http://plnkr.co/edit/UeUGE88LuQRgeo7I412p?p=preview
This is beacuse you are using one way binding in your directive (&) so changes to the model are not tracked. Try using two way binding = and it should work as expected.
The & binding allows a directive to trigger evaluation of an expression in the context of the original scope, at a specific time. Any legal expression is allowed, including an expression which contains a function call. Because of this, & bindings are ideal for binding callback functions to directive behaviors.
...
return {
restrict: 'A',
scope: {
test: '='
},
link: function (scope, element, attrs) {
element.text(scope.text);
scope.$watch('test', function (value) {
console.log('WATCH FIRED!');
element.text(scope.test);
});
}
};
Plnkr
Or you would need to do:-
scope.$watch(function(){
return scope.test();
}, function (value) {
console.log('WATCH FIRED!');
element.text(scope.test);
});
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