Angularjs >1.3 $validator causes modelValue to go undefined - angularjs

I am using $validator to write a custom form validation directive.
Currently it looks like this :
module.directive('tweetLength', function(URLDetector) {
return {
restrict: 'A',
require: 'ngModel',
link: function(scope, element, attrs, ctrl) {
var allowedCharacters;
allowedCharacters = parseInt(attrs.tweetLength);
ctrl.$validators.tweetLength = function(modelValue, viewValue) {
var result;
return result = URLDetector.urlAdjustedCharacterCount(modelValue) <= allowedCharacters;
};
}
};
});
It checks the model of the element it is attached to for the number of characters, whilst taking into account link shortening (so ng-minlength and ng-maxlength don't work). It returns false when the requirements aren't met. The problem is that when it returns false modelValue goes undefined. I know at this point the value is meant to be stored in $$invalidModelValue, but I still need the value in the original model since it is being used elsewhere in the view.
Is there a way to stop Angular from moving it and making the original model undefined? I know this problem could be solved in the form controller, but I don't think that is the correct way to do it since I want to disable the form submission button using the form state and not some external variable. Is there an alternate way to approach this problem whilst using Angular form validation?

Beginning in Angular v. 1.3, when $validate() returns false, the value of ng-model is set to undefined (docs here)
To prevent this behavior, set allowInvalid property of ngModelOptions to true, like so:
ng-model-options="{allowInvalid: true}"

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 to use the last valid modelValue if a model becomes invalid?

I'm working on an application that saves changes automatically when the user changes something, for example the value of an input field. I have written a autosave directive that is added to all form fields that should trigger save events automatically.
template:
<input ng-model="fooCtrl.name" autosave>
<input ng-model="fooCtrl.email" autosave>
directive:
.directive('autosave', ['$parse', function ($parse) {
return {
restrict: 'A',
require: 'ngModel',
link: function (scope, element, attrs, ngModel) {
function saveIfModelChanged () {
// save object containing name and email to server ...
}
ngModel.$viewChangeListeners.push(function () {
saveIfModelChanged();
});
}
};
}]);
So far, this all works fine for me. However, when I add validation into the mix, for example validating the input field to be a valid email address, the modelValue is set to undefined as soon as the viewValue is changed to an invalid email address.
What I would like to do is this: Remember the last valid modelValue and use this when autosaving. If the user changes the email address to be invalid, the object containing name and email should still be saved to the server. Using the current valid name and the last valid email.
I started out by saving the last valid modelValue like this:
template with validation added:
<input type="email" ng-model="fooCtrl.name" autosave required>
<input ng-model="fooCtrl.email" autosave required>
directive with saving lastModelValue:
.directive('autosave', ['$parse', function ($parse) {
return {
restrict: 'A',
require: 'ngModel',
link: function (scope, element, attrs, ngModel) {
var lastModelValue;
function saveIfModelChanged () {
// remeber last valid modelValue
if (ngModel.$valid) {
lastModelValue = ngModel.$modelValue;
}
// save object containing current or last valid
// name and email to server ...
}
ngModel.$viewChangeListeners.push(function () {
saveIfModelChanged();
});
}
};
}]);
My question is, how to use lastModelValue while saving, but preserving the invalid value in the view?
EDIT:
Another possibility, as suggested by Jugnu below, would be wrapping and manipulating the build in validators.
I tried to following: wrap all existing validators and remember the last valid value, to restore it if validations fails:
Object.keys(ngModel.$validators).forEach(function(validatorName, index) {
var validator = ngModel.$validators[validatorName];
ngModel.$validators[validatorName] = createWrapper(validatorName, validator, ngModel);
});
function createWrapper(validatorName, validator, ngModel){
var lastValid;
return function (modelValue){
var result = validator(modelValue);
if(result) {
lastValid = modelValue;
}else{
// what to do here? maybe asign the value like this:
// $parse(attrs.ngModel).assign(scope, lastValid);
}
return result;
};
}
But I'm not sure how to continue with this approach either. Can I set the model value without AngularJS kicking in and try to validate that newly set value?
I have created a simple directive that serves as a wrapper on the ng-model directive and will keep always the latest valid model value. It's called valid-ng-model and should replace the usage of ng-model on places where you want to have the latest valid value.
I've created an example use case here, I hope you will like it. Any ideas for improvements are welcomed.
This is the implementation code for valid-ng-model directive.
app.directive('validNgModel', function ($compile) {
return {
terminal: true,
priority: 1000,
scope: {
validNgModel: '=validNgModel'
},
link: function link(scope, element, attrs) {
// NOTE: add ngModel directive with custom model defined on the isolate scope
scope.customNgModel = angular.copy(scope.validNgModel);
element.attr('ng-model', 'customNgModel');
element.removeAttr('valid-ng-model');
// NOTE: recompile the element without this directive
var compiledElement = $compile(element)(scope);
var ngModelCtrl = compiledElement.controller('ngModel');
// NOTE: Synchronizing (inner ngModel -> outside valid model)
scope.$watch('customNgModel', function (newModelValue) {
if (ngModelCtrl.$valid) {
scope.validNgModel = newModelValue;
}
});
// NOTE: Synchronizing (outside model -> inner ngModel)
scope.$watch('validNgModel', function (newOutsideModelValue) {
scope.customNgModel = newOutsideModelValue;
});
}
};
});
Edit: directive implementation without isolate scope: Plunker.
Since you are sending the entire object for each field modification, you have to keep the last valid state of that entire object somewhere. Use case I have in mind:
You have a valid object { name: 'Valid', email: 'Valid' }.
You change the name to invalid; the autosave directive placed at the name input knows its own last valid value, so the correct object gets sent.
You change the email to invalid too. The autosave directive placed at the email input knows its own last valid value but NOT that of name. If the last known good values are not centralized, an object like { name: 'inalid', email: 'Valid' } will be sent.
So the suggestion:
Keep a sanitized copy of the object you are editing. By sanitized I mean that any invalid initial values should be replaced by valid pristine ones (e.g. zeros, nulls etc). Expose that copy as a controller member, e.g. fooCtrl.lastKnowngood.
Let autosave know the last known good state, e.g. as:
<input ng-model="fooCtrl.email" autosave="fooCtrl.lastKnowngood" required />
Keep the last known good local value in that object; utilize the ng-model expression, e.g. as:
var lastKnownGoodExpr = $parse(attrs.autosave);
var modelExpr = $parse(attrs.ngModel);
function saveIfModelChanged () {
var lastKnownGood = lastKnownGoodExpr(scope);
if (ngModel.$valid) {
// trick here; explanation later
modelExpr.assign({fooCtrl: lastKnownGood}, ngModel.$modelValue);
}
// send the lastKnownGood object to the server!!!
}
Send the lastKnownGood object.
The trick, its shortcomings and how can it be improved: When setting the local model value to the lastKnownGood object you use a context object different than the current scope; this object assumes that the controller is called fooCtrl (see the line modelExpr.assign({fooCtrl: lastKnownGood}, ...)). If you want a more general directive, you may want to pass the root as a different attribute, e.g.:
<input ng-model="fooCtrl.email" autosave="fooCtrl.lastKnowngood" required
autosave-fake-root="fooCtrl" />
You may also do some parsing of the ng-model expression yourself to determine the first component, e.g. substring 0 → 1st occurence of the dot (again simplistic).
Another shortcoming is how you handle more complex paths (in the general case), e.g. fooCtrl.persons[13].address['home'].street - but that seems not to be your use case.
By the way, this:
ngModel.$viewChangeListeners.push(function () {
saveIfModelChanged();
});
can be simplified as:
ngModel.$viewChangeListeners.push(saveIfModelChanged);
Angular default validators will only assign value to model if its valid email address.To overcome that you will need to override default validators.
For more reference see : https://docs.angularjs.org/guide/forms#modifying-built-in-validators
You can create a directive that will assign invalide model value to some scope variable and then you can use it.
I have created a small demo for email validation but you can extend it to cover all other validator.
Here is fiddle : http://plnkr.co/edit/EwuyRI5uGlrGfyGxOibl?p=preview

Why does adding additional AngularJS validation directives cause $asyncValidators to run multiple times?

Why does adding additional AngularJS validation directives cause $asyncValidators to run multiple times on page load?
I created a custom directive which implements $asyncValidators. This is the basic structure of that custom directive:
myApp.directive('userSaved',['$q','userLookup',function($q, userLookup){
return {
restrict: 'A',
require: 'ngModel',
link: function(scope, elem, attrs, ctrl){
ctrl.$asyncValidators.userSaved = function(modelValue, viewValue) {
// do stuff
}
}
}
}]);
The controller initializes the tailNumber model value like this:
$scope.tailNumber = 'N33221';
This is the html where the user-saved directive runs 3 times on page load:
<input ng-model="tailNumber" name="tailNumber" user-saved
ng-minlength="2" ng-pattern="/^[A-z][a-zA-Z0-9]*$/" >
When I remove ng-minlength="2" then the user-saved directive runs twice on page load (2 times). This is the html with ng-minlength="2" removed:
<input ng-model="tailNumber" name="tailNumber" user-saved
ng-pattern="/^[A-z][a-zA-Z0-9]*$/" >
When I remove ng-pattern="/^[A-z][a-zA-Z0-9]*$/" then the user-saved directive only runs 1 time. This is the html after removing ng-pattern="/^[A-z][a-zA-Z0-9]*$/"
<input ng-model="tailNumber" name="tailNumber" user-saved >
Why does my function registered with $asyncValidators run an additional time for each additional ng validator attached to the form element?
My custom directive is an expensive $http call, and I prefer my custom directive only run once on page load. Is it possible to use all of these ng validators and while only running my async validator function one time instead of 3 times on page load?
This is because validation directives like ngMaxlength, ngPattern invoke an initial validation cycle with a call to ngModelController.$validate().
This causes all the validation directive to run their validation logic, including the async validators.
One way to prevent the redundant $http calls, and in fact it is a good practice anyway, is to cache the validation result for each input.
It actually took me a while to figure this one out. As mentioned in this post, Angular validators trigger additional validations. I decided not to fight this behavior and work around it instead, falling back to parsers and formatters:
myApp.directive('userSaved',['$q','dataservice',function($q, dataservice){
return {
restrict: 'A',
require: 'ngModel',
link: function(scope, elem, attrs, ctrl){
ctrl.$parsers.unshift(checkUserSaved);
ctrl.$formatters.unshift(checkUserSaved);
function checkUserSaved(value){
ctrl.$setValidity("usersaved") // the absence of the state parameter sets $pending to true for this validation
dataservice.getUserSaved(value).then(function(response){
var userIsSaved = (response === true);
ctrl.$setValidity("usersaved", userIsSaved); // the presence of the state parameter removes $pending for this validation
return userIsSaved ? value : undefined;
});
return value;
}
}
}
}]);
As a reference, you also might want to check the Angular docs
EDIT
Upon further investigation, it appears that in the case of ng-pattern the extra validations are only triggered when the regex is converted from a string.
Passing the regex directly:
<div ng-pattern="/^[0-9]$/" user-saved></div>
fixed the problem for me while making use of the validators pipeline.
For reference, see this github issue
I followed #New Dev's advice and implemented a simple caching routine which fulfilled my requirement quite nicely, here's what I came up with ..
link: function (scope, element, attributes, ngModel) {
var cache = {};
ngModel.$asyncValidators.validateValue = function (modelValue) {
if (modelValue && cache[modelValue] !== true) {
return MyHttpService.validateValue(modelValue).then(function (resolved) {
cache[modelValue] = true; // cache
return resolved;
}, function(rejected) {
cache[modelValue] = false;
return $q.reject(rejected);
});
} else {
return $q.resolve("OK");
}
};
}

ngModel and How it is Used

I am just getting started with angular and ran into the directive below. I read a few tutorials already and am reading some now, but I really don't understand what "require: ngModel" does, mainly because I have no idea what ngModel does overall. Now, if I am not insane, it's the same directive that provides two way binding (the whole $scope.blah = "blah blah" inside ctrl, and then {{blah}} to show 'blah blah' inside an html element controlled by directive.
That doesn't help me here. Furthermore, I don't understand what "model: '#ngModel' does. #ngModel implies a variable on the parents scope, but ngModel isn't a variable there.
tl;dr:
What does "require: ngModel" do?
What does "model : '#ngModel'" do?
*auth is a service that passes profile's dateFormat property (irrelevant to q)
Thanks in advance for any help.
angular.module('app').directive('directiveDate', function($filter, auth) {
return {
require: 'ngModel',
scope: {
model : '#ngModel',
search: '=?search'
},
restrict: 'E',
replace: true,
template: '<span>{{ search }}</span>',
link: function($scope) {
$scope.set = function () {
$scope.text = $filter('date')($scope.model, auth.profile.dateFormat );
$scope.search = $scope.text;
};
$scope.$watch( function(){ return $scope.model; }, function () {
$scope.set();
}, true );
//update if locale changes
$scope.$on('$localeChangeSuccess', function () {
$scope.set();
});
}
};
});
ngModel is an Angular directive responsible for data-binding. Through its controller, ngModelController, it's possible to create directives that render and/or update the model.
Take a look at the following code. It's a very simple numeric up and down control. Its job is to render the model and update it when the user clicks on the + and - buttons.
app.directive('numberInput', function() {
return {
require: 'ngModel',
restrict: 'E',
template: '<span></span><button>+</button><button>-</button>',
link: function(scope, element, attrs, ngModelCtrl) {
var span = element.find('span'),
plusButton = element.find('button').eq(0),
minusButton = element.find('button').eq(1);
ngModelCtrl.$render = function(value) {
updateValue();
};
plusButton.on('click', function() {
ngModelCtrl.$setViewValue(ngModelCtrl.$modelValue + 1);
updateValue();
});
minusButton.on('click', function() {
ngModelCtrl.$setViewValue(ngModelCtrl.$modelValue - 1);
updateValue();
});
function updateValue(value) {
span.html(ngModelCtrl.$modelValue);
}
}
};
});
Working Plunker
Since it interacts with the model, we can use ngModelController. To do that, we use the require option to tell Angular we want it to inject that controller into the link function as its fourth argument. Now, ngModelController has a vast API and I won't get into much detail here. All we need for this example are two methods, $render and $setViewValue, and one property, $modelValue.
$render and $setViewValue are two ways of the same road. $render is called by Angular every time the model changes elsewhere so the directive can (re)render it, and $setViewValue should be called by the directive every time the user does something that should change the model's value. And $modelValue is the current value of the model. The rest of the code is pretty much self-explanatory.
Finally, ngModelController has an arguably shortcoming: it doesn't work well with "reference" types (arrays, objects, etc). So if you have a directive that binds to, say, an array, and that array later changes (for instance, an item is added), Angular won't call $render and the directive won't know it should update the model representation. The same is true if your directive adds/removes an item to/from the array and call $setViewValue: Angular won't update the model because it'll think nothing has changed (although the array's content has changed, its reference remains the same).
This should get you started. I suggest that you read the ngModelController documentation and the official guide on directives so you can understand better how this all works.
P.S: The directive you have posted above isn't using ngModelController at all, so the require: 'ngModel' line is useless. It's simply accessing the ng-model attribute to get its value.

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