paisley.london paisley.london - 3 months ago 117
AngularJS Question

Allow only numbers in range to be entered into text box

I'm trying to create input text directive that'll only accept numbers within a specific range. I've tried parsing the value as an integer, of course

min
and
max
didn't work.

I do not want to use input[type="number"].

Ultimately, I'm trying to create a date of birth free input text field.
Like the one seen below:



date-of-birth.png

The directive I've adapted [which i'm trying to use at the moment] - the original can be found @ angularjs: allows only numbers to be typed into a text box

app.directive('onlyDigits', function () {
return {
restrict: 'A',
require: '?ngModel',
link: function (scope, element, attrs, modelCtrl) {
modelCtrl.$parsers.push(function (inputValue) {
if (inputValue == undefined) return '';
var transformedInput = inputValue.replace(/[^0-9]/g, '');
var theInt = parseInt(transformedInput);
if (transformedInput !== inputValue) {
modelCtrl.$setViewValue(transformedInput);
modelCtrl.$render();
}
return theInt;
});
}
};


What I hoped to do after I've solved this, is to do a conditional
ng-show
, to show an error for a span element - when the user has typed a value over 31 (for day) 12 (for month) and so forth.

I welcome any suggestions.

Thank you.

Answer

I had the exact same problem. I tried "everything" to make it both user friendly and to not accept invalid values. Finally I gave up on apparently easy solutions, like ng-pattern, and with help of a friend, we came up with this integers-only directive.

It uses type=text, supports both min and max, do not accept chars beyond numbers and - (as a first character in case minimum is negative) to be typed.

Also, ng-model is never assigned with invalid value such as empty string or NaN, only values between given range or null.

I know, at first it looks rather intimidating ;)

HTML

// note: uses underscore.js
<body>
  <form name="form">
    <header>DD / MM / YYYY</header>
    <section>
      <input type="text" 
             name="day" 
             ng-model="day" 
             min="1" 
             max="31" 
             integers-only>
      <input type="text" 
             name="month" 
             ng-model="month" 
             min="1" 
             max="12" 
             integers-only>
      <input type="text" 
             name="year" 
             ng-model="year" 
             min="1900" 
             max="2016" 
             integers-only> 
    </section>
    <section>
      <span ng-show="form.day.$invalid">Invalid day</span>
      <span ng-show="form.month.$invalid">Invalid month</span>
      <span ng-show="form.year.$invalid">Invalid year</span>
    </section>
  </form> 
</body>

JavaScript

/**
 * numeric input
 * <input type="text" name="name" ng-model="model" min="0" max="100" integers-only>
 */
angular.module('app', [])
.directive('integersOnly', function() {
  return {
    restrict: 'A',
    require: 'ngModel',
    scope: {
        min: '=',
        max: '='
    },
    link: function(scope, element, attrs, modelCtrl) {
      function isInvalid(value) {
        return (value === null || typeof value === 'undefined' || !value.length);
      }

      function replace(value) {
        if (isInvalid(value)) {
          return null;
        }

        var newValue = [];
        var chrs = value.split('');
        var allowedChars = ['0','1','2','3','4','5','6','7','8','9','-'];

        for (var index = 0; index < chrs.length; index++) {
          if (_.contains(allowedChars, chrs[index])) {
            if (index > 0 && chrs[index] === '-') {
              break;
            }
            newValue.push(chrs[index]);
          }
        }

        return newValue.join('') || null;
      }

      modelCtrl.$parsers.push(function(value) {
        var originalValue = value;

        value = replace(value);

        if (value !== originalValue) {
          modelCtrl.$setViewValue(value);
          modelCtrl.$render();
        }

        return value && isFinite(value) ? parseInt(value) : value;
      });

      modelCtrl.$formatters.push(function(value) {
        if (value === null || typeof value === 'undefined') {
          return null;
        }

        return parseInt(value);
      });
      modelCtrl.$validators.min = function(modelValue) {
        if (scope.min !== null && modelValue !== null && modelValue < scope.min) { return false; }
        return true;
      };
      modelCtrl.$validators.max = function(modelValue) {
        if (scope.max !== null && modelValue !== null && modelValue > scope.max) { return false; }
        return true;
      };
      modelCtrl.$validators.hasOnlyChar = function(modelValue) {
        if (!isInvalid(modelValue) && modelValue === '-') { return false; }
        return true;
      };
    }
  };
});

Result

image


Related plunker here http://plnkr.co/edit/mIiKuw