AngularJS how do I watch $viewValue instead of ngModel?

Problem

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');
            });
        });
    }
Problem courtesy of: miyasudokoro

Solution

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.

Solution courtesy of: Simeon Cheeseman

Discussion

This will watch the $viewValue of a ngModelController if the tag has the ng-model and match attribute. It is also possible to watch other ngModelController $viewValues, like in a form with many input directives.

    (function () {
        'use strict';

        angular
            .module('app.commons')
            .directive('match', MatchValidator);

        function MatchValidator() {
            return {
                require: 'ngModel',
                link: LinkFunction
            };

            function LinkFunction(scope, element, attrs, ngModel) {
                scope.$watch(function(){
                        return ngModel.$viewValue;
                    }, 
                    function(newValue, oldValue){
                        // do something
                    }
                );
            }
        }
    })();
Discussion courtesy of: Lusk116

Personally, I had to use $formatters to avoid waiting for the debounce.

Discussion courtesy of: user1464581

This recipe can be found in it's original form on Stack Over Flow.