3

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.

Community
  • 1
  • 1
paisley.london
  • 121
  • 1
  • 9
  • What is the reason that you don't want `type="number"`? Because of tiny scroll buttons that appear in number input? – oKonyk Aug 10 '16 at 16:28
  • Hey thanks for the message. No, I can hide those with css. Because the `minlength` and `maxlength` `attrs` don't work with `type="number"` and if I did use those attrs manually then, it then requires more DOM manipulation via jQuery, so I'm trying to see if there's a more angular way of doing this with the input type staying as `text`. – paisley.london Aug 10 '16 at 16:34
  • if you want to make the text filed to act as number filed i do have a solution for that but not in angular way rather in javascript way all you have to do is to check if your charCode is in range you specify. if you want i can psot the code here – Joel Joseph Aug 10 '16 at 16:45

3 Answers3

3

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 @Teemu Turkia, we came up with 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

Mikko Viitala
  • 8,344
  • 4
  • 37
  • 62
  • 1
    Thank you so much for your help @Mikko !, you're a genius! **this worked like a charm!** FYI to anyone else using this, download [underscore.js](http://underscorejs.org/) so that the `_.contains()` function works inside the directive! – paisley.london Aug 11 '16 at 10:07
2

Here is solution without any custom directives. It's still input type="number" but needed functionality is achieved.

Here is plunker

<!DOCTYPE html>
<html>
  <head></head>

  <body ng-app="app" ng-controller="dobController as dob">
    <h3>Date of birth form</h3>
    <form name="dobForm" class="form" novalidate="">
        <div>
            <label for="date">DD</label>
            <input  type="number" ng-model="dob.date" name="date" min="1" max="31" integer />
            <label for="month">MM</label>
            <input  type="number" ng-model="dob.month" name="month" min="1" max="12" integer />
            <label for="year">YYYY</label>
            <input  type="number" ng-model="dob.year" name="year" min="1900" max="2016" integer />
            
            <div style="color: red;"  ng-if="dobForm.$invalid">
              <p ng-show="dobForm.date.$error.min || dobForm.date.$error.max">
                date must be in range 1 to 31!
              </p>
              <p ng-show="dobForm.month.$error.min || dobForm.month.$error.max">
                month must be in range 1 to 12!
              </p>
              <p ng-show="dobForm.year.$error.min || dobForm.year.$error.max">
                year must be in range 1900 to 2016!
              </p>
            </div>
            
          </div>
    </form>
    
    <script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.5.0/angular.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.5.0/angular-messages.js"></script>
    <script>
       var app = angular.module('app', []);

       app.controller('dobController',  function($scope) {});
    </script>
    <style>
        input[type=number]::-webkit-inner-spin-button, 
        input[type=number]::-webkit-outer-spin-button { 
          -webkit-appearance: none; 
          margin: 0; 
        }
    </style>
    
  </body>

</html>

enter image description here

oKonyk
  • 1,468
  • 11
  • 16
  • And that's the reason why this is so hard! I mean this is nice solution but it still accepts e.g. consecutive `+` and `-` to be typed w/o giving $error, and also `.` and `,`. See here http://i.imgur.com/MsqGQ38.png – Mikko Viitala Aug 11 '16 at 16:46
  • Yep, I completely agree with this point. Accepted solution is far more comprehensive. – oKonyk Aug 11 '16 at 20:23
0

This solution uses the min and max attributes to limit values of the input fields. It also uses ngModelOptions to update the model value only after a defined interval. This is to allow users to type in values before the model parser acts on the input.

angular.module("app", []);
angular.module("app").directive('onlyDigits', function() {
  return {
    restrict: 'A',
    require: '?ngModel',
    scope: {
      min: "@",
      max: "@"
    },
    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);
        var max = scope.max;
        var min = scope.min;
        if (theInt > max) {
          theInt = max;
        } else if (theInt < min) {
          theInt = min;
        }
        modelCtrl.$setViewValue(theInt.toString());
        modelCtrl.$render();
        return theInt;
      });
    }
  }
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.8/angular.min.js"></script>

<body ng-app="app">
  <input type="text" ng-model="month" ng-model-options="{ debounce: 200 }" only-digits min="1" max="12">
  <input type="text" ng-model="day" ng-model-options="{ debounce: 200 }" min="1" max="30" only-digits>
  <input type="text" ng-model="year" ng-model-options="{ debounce: 500 }" only-digits min="1900" max="2050">

</body>
10100111001
  • 1,832
  • 1
  • 11
  • 7