Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

AngularJS how do I watch $viewValue instead of ngModel?

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');
            });
        });
    }
like image 521
miyasudokoro Avatar asked Aug 06 '14 23:08

miyasudokoro


People also ask

What is $viewValue in AngularJS?

$viewValue is the current value in the view - your string input.

What is the difference between ngValue and ngModel?

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.

What is $setValidity in AngularJS?

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.

How do you use ngModel labels?

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.


2 Answers

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.

like image 102
Simeon Cheeseman Avatar answered Sep 22 '22 12:09

Simeon Cheeseman


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.

like image 30
Noppey Avatar answered Sep 21 '22 12:09

Noppey