ngModel updated after ngBlur - angularjs

I'm using ui-bootstrap's datepicker input with AngularJS. It has an ngModel and a validatioon method bound to ngBlur event:
<input id="startDate" type="text" ng-model="myCtrl.startDate" ng-blur="myCtrl.validateDate()"/>
The validation method checks the ngModel according to set of rules in the controller, and alerts if needed. Here is the related part of the controller:
var vm = this;
vm.startDate = new Date();
vm.minStartDate = new Date(2000,1,1);
vm.validateDate = function(){
if(vm.startDate < vm.minStartDate)
alert("Start date can't be earliar than 2000");
}
The problem is that, the validation method runs before the update of the model. Example scenario is as follows:
The rule: startDate can't be earlier than year 2000.
When the page loads, startDate value is initially set to today. The user selects 31.12.1999 from the calendar, ngBlur method is called. However, ngModel is not yet updated. Thus the validation checks today's value and says ok. Actually, it should have checked ngModel's value with 31.12.1999. The value changes simultaneously on the input view, but in the background, it is set after the blur method is fired. As far as I know, two-way binding in AngularJS normally serves for this purpose but the ngBlur must be an exception.
I have tried many thing such as;
Adding ng-model-options to the component:
ng-model-options="{ updateOn: 'blur' }"
Using pre and post linking by modifying the input element
Using ng-change instead of ng-blur
However, none of the above works, and still ng-blur runs before the model is updated.

I have found the solution by manually updating the values inside the controller. Obviously AngularJS won't update the model with the value inside the HMTL element until blur event is fired. So, it's better to write a method to update it.
I can access the value inside the element as follows:
vm.startDate = angular.element('#startDate').val();
Then I can wrap this with a method:
vm.updateModels() = function(){
vm.startDate = angular.element('#startDate').val();
}
Finally, I just call this at the very first of my blur event:
vm.validateDate = function(){
//Update first
vm.updateModels();
//Then validate
if(vm.startDate < vm.minStartDate)
alert("Start date can't be earliar than 2000");
}
Ta tammm, now I can validate the updatet values!

A simple way to make sure that your validation fires only once the value is updated would be to call it on an ng-change instead of ng-blur. That way you are sure that the controller evaluates only once the ng-model value has been updated.

Try to build you own custom validator (directive)
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;
};
}
};
});
Check this example
https://plnkr.co/edit/SZoBKakhzIq5wRoCvEyG?p=preview

Related

Why does the ngModel validator code seem to run before the scope.$watch changes?

I am attempting to create an AngularJS directive with a custom validator so that I can show error messages based on the validator. However, I am running into an error because it seems that the validator is running prior to the scope.$watch() per the console.log() messages I've input.
Here is the directive:
angular
.module('app')
.directive('validateRefundAmount', validateRefundAmount);
validateRefundAmount.$inject = [ 'AmountConversionService', '$q' ];
function validateRefundAmount(AmountConversionService, $q) {
return {
restrict: 'A',
require: 'ngModel',
link: function (scope, element, attrs, control) {
scope.$watch('orderDetails.refundAmount', function(newValue, oldValue) {
if (newValue === oldValue) {
console.log(newValue, oldValue);
return;
}
// Get Already Refunded Amount
var refundedAmount = scope.orderDetails.refundedAmount;
// Converts Amount To Pure Integers, Removing Decimal
var totalPaymentAmount = AmountConversionService.prepareAmountForCalculations(scope.orderDetails.paymentAmount, 10);
var totalRefundAmount = AmountConversionService.prepareAmountForCalculations(newValue || 0);
// Add Already Refunded Amount to Previously Refunded Amount to Get Total Refund
if (refundedAmount) {
totalRefundAmount += AmountConversionService.prepareAmountForCalculations(refundedAmount);
}
control.$validators.refundAmountTooHigh = function() {
if (totalRefundAmount > totalPaymentAmount) {
return false
}
return true;
}
});
}
};
}
The element that this is applied to is an text input box that starts with no value. When i type '1' into the field the validator doesn't run. When I add a '2' to the field, making '12', the validator runs with '1' as the input. The same thing occurs when I add a '3' to the field making '123' -- it runs with '12', instead of the new value of '123'.
When I've inserted console.log() statements to see what is occurring when, it looks like the validator code runs and then the scope.watch() runs creating the new value after the validator has run but I don't know why or what to search for to find out.
There may be another way to do what I'm trying to do -- what I want to do is run the validator everytime the element value changes. I can't use bind on keypress due to keypress not detecting backspaces and may be able to use bind on keydown -- not sure.
Please check doc for $setViewValue, apparently $validators is used before ngModelValue update.
And your scope.$watch will only be invoked after $modelValue is updated.
https://docs.angularjs.org/api/ng/type/ngModel.NgModelController#$setViewValue
When $setViewValue is called, the new value will be staged for committing through the $parsers and $validators pipelines. If there are no special ngModelOptions specified then the staged value is sent directly for processing through the $parsers pipeline. After this, the $validators and $asyncValidators are called and the value is applied to $modelValue. Finally, the value is set to the expression specified in the ng-model attribute and all the registered change listeners, in the $viewChangeListeners list are called.

How do I specify an angular directive in the link of another?

I'm trying to create a server-validate directive, which asynchronously validates form input by submitting a partial form to our server back-end and parsing the response. I hoped to use it like this:
<input type="text" ng-model="stuff" server-validate />
(I'm combining it with another directive on the wrapping <form>, that specifies what URL to use etc...) In order for the form not to submit validation requests on page load, I need to set ng-model-options="{ updateOn: 'blur' }", but I'd like to *not* have to do this on every element in the form. Instead, I'd like theserver-validate` to specify this behavior as well.
I've tried a couple of things in the link function, for example attrs['ngModelOptions'] = '{updateOn: "blur"}' and attrs['ngModelOptions'] = { updateOn: 'blur' }, but neither had any effect at all.
Is there a way to apply this through my own directive, without having to specify anything else?
They have a great example of what you want over at the docs:
directive('contenteditable', ['$sce', function($sce) {
return {
restrict: 'A', // only activate on element attribute
require: '?ngModel', // get a hold of NgModelController
link: function(scope, element, attrs, ngModel) {
if (!ngModel) return; // do nothing if no ng-model
// Specify how UI should be updated
ngModel.$render = function() {
element.html($sce.getTrustedHtml(ngModel.$viewValue || ''));
};
// Listen for change events to enable binding
element.on('blur keyup change', function() {
scope.$evalAsync(read);
});
read(); // initialize
// Write data to the model
function read() {
var html = element.html();
// When we clear the content editable the browser leaves a <br> behind
// If strip-br attribute is provided then we strip this out
if ( attrs.stripBr && html == '<br>' ) {
html = '';
}
ngModel.$setViewValue(html);
}
}
};
}]);
So the only thing that looks like what you would change would be to remove the keyup and change events from the element.on. Then in your blur you would also do the server request. Docs on ngModelController: https://docs.angularjs.org/api/ng/type/ngModel.NgModelController

How to implement an ng-change for a custom directive

I have a directive with a template like
<div>
<div ng-repeat="item in items" ng-click="updateModel(item)">
<div>
My directive is declared as:
return {
templateUrl: '...',
restrict: 'E',
require: '^ngModel',
scope: {
items: '=',
ngModel: '=',
ngChange: '&'
},
link: function postLink(scope, element, attrs)
{
scope.updateModel = function(item)
{
scope.ngModel = item;
scope.ngChange();
}
}
}
I would like to have ng-change called when an item is clicked and the value of foo has been changed already.
That is, if my directive is implemented as:
<my-directive items=items ng-model="foo" ng-change="bar(foo)"></my-directive>
I would expect to call bar when the value of foo has been updated.
With code given above, ngChange is successfully called, but it is called with the old value of foo instead of the new updated value.
One way to solve the problem is to call ngChange inside a timeout to execute it at some point in the future, when the value of foo has been already changed. But this solution make me loose control over the order in which things are supposed to be executed and I assume that there should be a more elegant solution.
I could also use a watcher over foo in the parent scope, but this solution doesn't really give an ngChange method to be implmented and I have been told that watchers are great memory consumers.
Is there a way to make ngChange be executed synchronously without a timeout or a watcher?
Example: http://plnkr.co/edit/8H6QDO8OYiOyOx8efhyJ?p=preview
If you require ngModel you can just call $setViewValue on the ngModelController, which implicitly evaluates ng-change. The fourth parameter to the linking function should be the ngModelCtrl. The following code will make ng-change work for your directive.
link : function(scope, element, attrs, ngModelCtrl){
scope.updateModel = function(item) {
ngModelCtrl.$setViewValue(item);
}
}
In order for your solution to work, please remove ngChange and ngModel from isolate scope of myDirective.
Here's a plunk: http://plnkr.co/edit/UefUzOo88MwOMkpgeX07?p=preview
tl;dr
In my experience you just need to inherit from the ngModelCtrl. the ng-change expression will be automatically evaluated when you use the method ngModelCtrl.$setViewValue
angular.module("myApp").directive("myDirective", function(){
return {
require:"^ngModel", // this is important,
scope:{
... // put the variables you need here but DO NOT have a variable named ngModel or ngChange
},
link: function(scope, elt, attrs, ctrl){ // ctrl here is the ngModelCtrl
scope.setValue = function(value){
ctrl.$setViewValue(value); // this line will automatically eval your ng-change
};
}
};
});
More precisely
ng-change is evaluated during the ngModelCtrl.$commitViewValue() IF the object reference of your ngModel has changed. the method $commitViewValue() is called automatically by $setViewValue(value, trigger) if you do not use the trigger argument or have not precised any ngModelOptions.
I specified that the ng-change would be automatically triggered if the reference of the $viewValue changed. When your ngModel is a string or an int, you don't have to worry about it. If your ngModel is an object and your just changing some of its properties, then $setViewValue will not eval ngChange.
If we take the code example from the start of the post
scope.setValue = function(value){
ctrl.$setViewValue(value); // this line will automatically evalyour ng-change
};
scope.updateValue = function(prop1Value){
var vv = ctrl.$viewValue;
vv.prop1 = prop1Value;
ctrl.$setViewValue(vv); // this line won't eval the ng-change expression
};
After some research, it seems that the best approach is to use $timeout(callback, 0).
It automatically launches a $digest cycle just after the callback is executed.
So, in my case, the solution was to use
$timeout(scope.ngChange, 0);
This way, it doesn't matter what is the signature of your callback, it will be executed just as you defined it in the parent scope.
Here is the plunkr with such changes: http://plnkr.co/edit/9MGptJpSQslk8g8tD2bZ?p=preview
Samuli Ulmanen and lucienBertin's answers nail it, although a bit of further reading in the AngularJS documentation provides further advise on how to handle this (see https://docs.angularjs.org/api/ng/type/ngModel.NgModelController).
Specifically in the cases where you are passing objects to $setViewValue(myObj). AngularJS Documentatation states:
When used with standard inputs, the view value will always be a string (which is in some cases parsed into another type, such as a Date object for input[date].) However, custom controls might also pass objects to this method. In this case, we should make a copy of the object before passing it to $setViewValue. This is because ngModel does not perform a deep watch of objects, it only looks for a change of identity. If you only change the property of the object then ngModel will not realize that the object has changed and will not invoke the $parsers and $validators pipelines. For this reason, you should not change properties of the copy once it has been passed to $setViewValue. Otherwise you may cause the model value on the scope to change incorrectly.
For my specific case, my model is a moment date object, so I must clone the object first before then calling setViewValue. I am lucky here as moment provides a simple clone method: var b = moment(a);
link : function(scope, elements, attrs, ctrl) {
scope.updateModel = function (value) {
if (ctrl.$viewValue == value) {
var copyOfObject = moment(value);
ctrl.$setViewValue(copyOfObject);
}
else
{
ctrl.$setViewValue(value);
}
};
}
The fundamental issue here is that the underlying model does not get updated until the digest cycle that happens after scope.updateModel has finished executing. If the ngChange function requires details of the update that is being made then those details can be made available explicitly to ngChange, rather than relying on the model updating having been previously applied.
This can be done by providing a map of local variable names to values when calling ngChange. In this scenario, you can mapping the new value of the model to a name which can be referenced in the ng-change expression.
For example:
scope.updateModel = function(item)
{
scope.ngModel = item;
scope.ngChange({newValue: item});
}
In the HTML:
<my-directive ng-model="foo" items=items ng-change="bar(newValue)"></my-directive>
See: http://plnkr.co/edit/4CQBEV1S2wFFwKWbWec3?p=preview

AngularJS directive not being trigged on manual change to a field value

I've got an input field and a button that manually updates the input field with a value. The field also has a directive that monitors changes for error handling.
When typing into the input field, the directive triggers just fine. When the button is pressed to manually update the input field, the directive does not get triggered, and I can't figure out why. This is causing my validation to not update, sometimes causing an error to persist when the value is actually not an error.
Here is the input field in question:
<input testchange type="text" name="hello" ng-model="formData.hello"/>
Here is the button to update the value:
update text
Here is the directive:
angular.module('app').directive('testchange', function() {
return {
require: 'ngModel',
link: function(scope, elm, attrs, ctrl) {
ctrl.$parsers.unshift(function(viewValue) {
console.log('value changed to ' + viewValue);
scope.history.push(viewValue);
})
}
}
});
I have made a simple example of the problem: http://jsfiddle.net/gakman/7zF9j/5/
You need to use a formatter in the case when you are updating the model value from button click. Understand your are updating model now not the view when you set formData.hello='hello world'.
You need to create a formatter and push it into $formatters collection on ngModelController. See documentation http://docs.angularjs.org/api/ng/type/ngModel.NgModelController
See my updated fiddle http://jsfiddle.net/cmyworld/N57JM/

How to update an input's value after calling $setViewValue

I have a form input with a ng-model as well as my custom directive which reads cookie data and should set the input value:
<form>
<input type="text" id="name" ng-model="name" my-cookie-directive>
</form>
My directive:
angular.module('myApp.directives').directive('myCookieDirective', ['CookieService', function(CookieService) {
return {
require: 'ngModel',
link: function(scope, elem, attrs, ctrl) {
var cookieVal = CookieService.getCookie(attrs.ngModel);
if(cookieVal != '') {
ctrl.$setViewValue(cookieVal);
elem.val(cookieVal); //not very cool hum?
}
}
};
}]);
When logging ctrl.$modelValue I can see that the right cookie data was set to my controller variable name but the input stays blank. I know that $setViewValue does not trigger a $digest and therefore tried ctrl.$render() but nothing happens.
I ended up using jQuery to set the input's value which is not satisfying at all.
Thanks in advance :)
You are correct in not wanting to use jQuery to set the input's value. Why would you be using Angular if you are going to do that then?
You can see a new Plunker here, using a different approach to the ones being mentioned. My suggestion: use NgModelController when you want to handle validations and format the model value.
For your current situation, you can use an isolated scope in the directive, and pass to it the scope property you want to update with the cookie value. Then in the directive, you can simply do:
scope.cookieField = cookieVal;
And Angular will handle the data binding and update the view value to match the model value. Plus, this is completely reusable.
Use $render and wrap everything in a function passed to $evalAsync:
if(cookieVal !== '') {
scope.$evalAsync(function(){
ctrl.$setViewValue(cookieVal);
ctrl.$render();
});
}
Plunker demo

Resources