I am using debounce on an input:
<input type="text"
ng-model="model.qty"
ng-model-options="{ debounce : 1000 }"
min="{{model.min}}" max="{{model.max}}" step="1"
qty-input validate-model-setting>
I have a directive that handles the disabling of an increment button and a decrement button for this input:
app.directive('qtyInput', function() {
return {
restrict: 'A',
require: '?ngModel',
link: function(scope, element, attrs, ngModelCtrl) {
scope.$watch(attrs.ngModel, function(n, o) {
var val = parseInt(n);
if(!isNaN(val)) {
if(val + 1 > model.max) {
scope.quantityIncDisabled = true;
} else {
scope.quantityIncDisabled = false;
}
if(val - 1 < model.min) {
scope.quantityDecDisabled = true;
} else {
scope.quantityDecDisabled = false;
}
}
});
}
}
});
The problem is, the watch on this directive is looking at the model. I need it to look at the $viewValue
instead. This is because, due to debounce, there is a race condition between typing in the input and using the increment/decrement buttons. For example, you can keep on clicking the decrement button over and over for exactly 1 second after the input has reached 1 (the min), and only then, after the debounce finishes, will the decrement button become disabled. Instead, I want the button to be disabled immediately when the input reaches 1, not after waiting the full debounce second. My best guess is that this means putting a $watch
on the $viewValue
, but I don't know how to get to it in the directive.
For your reference, the buttons themselves avoid the race condition by changing the input's value and then triggering the input, which makes the debounce work fluidly with both typing in the input and using the buttons.
link: function(scope, element, attrs) {
element.bind('click', function() {
$timeout(function() {
var input = element.parent().find('input');
var changingTo = parseInt(input[0].value) + parseInt(scope.inc);
if(scope.inc < 0 || isNaN(changingTo)) {
var min = parseInt(input[0].min);
if(isNaN(changingTo)) {
changingTo = min;
} else if(changingTo < min) {
return;
}
} else {
var max = parseInt(input[0].max);
if(changingTo > max) {
return;
}
}
input[0].value = String(changingTo);
input.trigger('input');
});
});
}
$viewValue is the current value in the view - your string input.
Please note that 'ng-value' does have an advantage over hard-coding to the element attribute 'value', in that you can specify non-string types. For example, 'true' and 'false' will be stored to the model as booleans, rather than as strings. Also ngValue is a one-way binding, and ngModel is a two-way binding.
The $setValidity() function is a built-in AngularJS function that is used to assign a key/value combination that can be used to check the validity of a specific model value. The key in this case is “unique” and the value is either true or false.
Since ngModel should only be used with the inputs, since it involves two-way data binding. Label does not deal with user input, thus it does not require the ngModel. So if you want to bind a scope variable to the label then you can use expressions.
The docs have the answer: https://code.angularjs.org/1.2.19/docs/api/ng/type/ngModel.NgModelController
You want to use $viewChangeListeners
instead of $watch
like so (you might need to push to $formatters
instead depending on how debounce works):
ngModelCtrl.$viewChangeListeners.push(function(){
var val = parseInt(ngModelCtrl.$viewValue);
if(!isNaN(val)) {
...
}
});
Though I haven't played with the debounce option yet so I don't know if this will fix your problem, but this is how you watch the viewValue.
An alternative solution of watching the $viewValue is to allow invalid ng-model updates, then you can watch the model instead.
ng-model-options="{allowInvalid: true}"
see: https://docs.angularjs.org/api/ng/directive/ngModelOptions
It's not a direct answer to this question, but I think it will help in many of the usecases for which people are visiting this topic.
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