I am a strong advocate of best practices, especially when it comes to angular but I can't manage to use the brand new $validators
pipeline feature as it should be.
The case is quite simple: 1 input enhanced by a directive using $parser
, $formatter
and some $validators
:
<input name="number" type="text" ng-model="number" number>
Here is the (simplified) directive:
myApp.directive('number', [function() {
return {
restrict: 'A',
require: 'ngModel',
/*
* Must have higher priority than ngModel directive to make
* number (post)link function run after ngModel's one.
* ngModel's priority is 1.
*/
priority: 2,
link: function($scope, $element, $attrs, $controller) {
$controller.$parsers.push(function (value) {
return isFinite(value)? parseInt(value): undefined;
});
$controller.$formatters.push(function (value) {
return value.toString() || '';
});
$controller.$validators.minNumber = function(value) {
return value && value >= 1;
};
$controller.$validators.maxNumber = function(value) {
return value && value <= 10;
};
}
};
}]);
I made a little plunk to play with :)
The behavior I am trying to achieve is: Considering that the initial value stored in the scope is valid, prevent it from being corrupted if the user input is invalid. Keep the old one until a new valid one is set.
NB: Before angular 1.3, I was able to do this using ngModelController
API directly in $parser/$formatter
. I can still do that with 1.3, but that would not be "angular-way".
NB2: In my app I am not really using numbers, but quantities.The problem remains the same.
It looks like you want some parsing to happen after validation, setting the model to the last valid value rather than one derived from the view. However, I think the 1.3 pipeline works the other way around: parsing happens before validation.
So my answer is to just do it as you would do it in 1.2: using $parsers
to set the validation keys and to transform the user's input back to the most recent valid value.
The following directive does this, with an array of validators
specified within the directive that are run in order. If any of the previous validators fails, then the later ones don't run: it assumes one validation error can happen at a time.
Most relevant to your question, is that it maintains the last valid value in the model, and only overwrites if there are no validation errors occur.
myApp.directive('number', [function() {
return {
restrict: 'A',
require: 'ngModel',
/*
* Must have higher priority than ngModel directive to make
* number (post)link function run after ngModel's one.
* ngModel's priority is 1.
*/
priority: 2,
link: function($scope, $element, $attrs, $controller) {
var lastValid;
$controller.$parsers.push(function(value) {
value = parseInt(value);
lastValid = $controller.$modelValue;
var skip = false;
validators.forEach(function(validator) {
var isValid = skip || validator.validatorFn(value);
$controller.$setValidity(validator.key, isValid);
skip = skip || !isValid;
});
if ($controller.$valid) {
lastValid = value;
}
return lastValid;
});
$controller.$formatters.push(function(value) {
return value.toString() || '';
});
var validators = [{
key: 'isNumber',
validatorFn: function(value) {
return isFinite(value);
}
}, {
key: 'minNumber',
validatorFn: function(value) {
return value >= 1;
}
}, {
key: 'maxNumber',
validatorFn: function(value) {
return value <= 10;
}
}];
}
};
}]);
This can be seen working at http://plnkr.co/edit/iUbUCfJYDesX6SNGsAcg?p=preview
I think you are over-thinking this in terms of Angular-way vs. not Angular-way. Before 1.3 using $parsers
pipeline was the Angular-way and now it's not?
Well, the Angular-way is also that ng-model
sets the model to undefined
(by default) for invalid values. Follow that Angular-way direction and define another variable to store the "lastValid" value:
<input ng-model="foo" ng-maxlength="3"
ng-change="lastValidFoo = foo !== undefined ? foo : lastValidFoo"
ng-init="foo = lastValidFoo">
No need for a special directive and it works across the board in a way that doesn't try to circumvent what Angular is doing natively - i.e. the Angular-way. :)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With