How do you prevent bad user input on watched input fields in angular?

I have a watched input field on a grid with pagination. Something like X of 28 Pages.

I want the user to be able to change that input, but I also want to prevent bad input.

My checks are >= 1 or <= Max Pages (28 in this case). The input defaults to 1.

I accomplished this by comparing the new value against those constraints, if it fails, revert to the old value. The problem comes when someone wants to type in 20 lets say. This requires they delete the 1, and type 20. As soon as they delete 1, it fails the constraints and reverts back to 1 making impossible to type in 20.

Is there anyway to accomplish this without removing it from the $watch?

You could use a combination of <input type="number"> and your own directive that has a parser and a listener for the blur event. That way your watch will only get executed when the page number is a valid page, or once with null when the input is invalid, but the user can input whatever until the blur event fires. Something like this:

<!doctype html>
<html ng-app="myApp">
<head>
    <script src="http://code.jquery.com/jquery-1.9.1.min.js"></script>
    <script src="http://code.angularjs.org/1.0.5/angular.min.js"></script>
    <script>
    angular.module('myApp', []).controller('Ctrl', function($scope) {
        $scope.pageNumber = 1;
    })
    .directive('myPagenumber', function() {
        return {
            require: 'ngModel',
            link: function($scope, elem, attrs, ctrl) {
                $scope.$watch(attrs.ngModel, function(val) {
                    console.log('ng-model value: ' + val);
                });

                var parsePage = function(val) {
                    var num = parseInt(val, 10);
                    if (isNaN(num)) {
                        return null;
                    } else if (num > 28 || num < 1) {
                        return 1;
                    } else {
                        return num;
                    }
                }

                ctrl.$parsers.push(function(val) {
                    return parsePage(val);
                });

                elem.bind('blur', function() {
                    var page = parsePage(elem.val());
                    if (page === null) 
                        page = 1;

                    $scope.$apply(function() {
                        ctrl.$setViewValue(page);
                        ctrl.$render();
                    });
                });
            }
        };
    });
    </script>
</head>
<body ng-controller="Ctrl">
    <input type="number" ng-model="pageNumber" my-pagenumber>
</body>
</html>

I wrote an example for you:

var min = 1;
var max = 28;
$('.page').live('keydown', function (e){      
      var currentVal = $(this).val();
      //enter,tab, shift
      if(e.which == 37 || e.which == 39 || e.which == 9 || e.which == 8) {
          return;
      // key up
      } else if(e.which == 38){
          if(currentVal < max){
             currentVal++;
          }
          $(this).val(currentVal);
      //key down    
      } else if( e.which == 40) {
        if(currentVal > min){
             currentVal--;
          }
          $(this).val(currentVal);
      //only numbers    
      } else if(e.which >= 48 && e.which <= 57){
        var val = e.which - 48;
        if(e.target.selectionEnd == e.target.selectionStart) {
          val = currentVal.insert(e.target.selectionEnd, val);
        } else {
          val = currentVal.replace(currentVal.substr(getSelectionStart(e.target),getSelectionEnd(e.target)),val);
        }
        if(min<=val && val <= max) {
          $(this).val(val);
        }
      }
      e.preventDefault();
});

// utility functions
//get the start index of the user selection
function getSelectionStart(o) {
    if ( typeof o.selectionStart != 'undefined' )
      return o.selectionStart;

    // IE And FF Support
    o.focus();
    var range = o.createTextRange();
    range.moveToBookmark(document.selection.createRange().getBookmark());
    range.moveEnd('character', o.value.length);
    return o.value.length - range.text.length;
};
//get the end index of the user selection
function getSelectionEnd(o) {
    if ( typeof o.selectionEnd != 'undefined' )
      return o.selectionEnd;

    // IE And FF Support
    o.focus();
    var range = o.createTextRange();
    range.moveToBookmark(document.selection.createRange().getBookmark());
    range.moveStart('character', - o.value.length);
    return range.text.length;
};
/*
* Insert Text at a index
*/
String.prototype.insert = function (index, string) {
  if (index > 0)
    return this.substring(0, index) + string + this.substring(index, this.length);
  else
    return string + this;
};

animate example: http://jsfiddle.net/PVxqe/1/