jenryb
jenryb

Reputation: 2117

Properly filtering an input field in Angular

I found this directive for filtering a field for currency, so a user just needs to type and the decimal is implied.

It works well, except that a user is able to put in a letter if they press a letter twice. I don't want them to be able to put in a letter at all.

I tried changing the type="number" but then it would only allow 5 digits before disappearing inexplicably. You can see this at this plunker:

http://plnkr.co/edit/Y1olj7Cdz7XKqRmE7BFb?p=preview

So either I need to change something (the regex?) in this line:

var plainNumber = viewValue.replace(/[^\d|\-+]/g, '');

or somehow fix the type="number" to keep input past five digits. Anybody know what to do here?

Upvotes: 1

Views: 2652

Answers (2)

Laurel
Laurel

Reputation: 6173

I'm unfamiliar with AngularJS, but I can tell your regex is not exactly what you intended.

You have this: [^\d|\-+]

This is a negated char class. All non-digits, non +s, non -s, AND non |s will match. Also note that \d contains more than [0-9], because it will likely match Persian digits and such.

I would use: [^-+0-9]. By putting the - directly after the ^ it does not need to be escaped.

The problem with your solution is you're waiting for the characters to be typed before you remove them. The keystrokes may happen faster than your field can purge them.


Despite having no experience whatsoever in Angular or jQuery,

I made something that works perfectly, based off of here:

It allows 0-9 to be typed freely. When a + or - is typed, it is never added to the end. Instead, it appears at the beginning, overwriting any existing sign. Maybe you should just run the snippet and see yourself:

var app = angular.module('App', []);

app.controller('MainCtrl', function($scope) {});

app.directive('inputRestrictor', [function() {
  return {
    restrict: 'A',
    require: 'ngModel',
    link: function(scope, element, attr, ngModelCtrl) {
      var pattern = /[^+0-9-]/g;

      function fromUser(text) {
        if (!text)
          return text;

        var rep = /[+]/g;
        var rem = /[-]/g;
        rep.exec(text);
        rem.exec(text);
        var indexp = rep.lastIndex;
        var indexm = rem.lastIndex;
        text = text.replace(/[+-]/g, '');
        if (indexp > 0 || indexm > 0) {
          if (indexp > indexm) text = "+" + text;
          else text = "-" + text;
        }
        var transformedInput = text.replace(pattern, '');
        ngModelCtrl.$setViewValue(transformedInput);
        ngModelCtrl.$render();
        return transformedInput;
      }
      ngModelCtrl.$parsers.push(fromUser);
    }
  };
}]);
<!DOCTYPE html>
<html>

  <head>
    <script data-require="[email protected]" data-semver="1.3.0-rc.2" src="  https://code.angularjs.org/1.3.0-rc.2/angular.js"></script>
    <link rel="stylesheet" href="style.css" />
    <script src="script.js"></script>
  </head>

  <body ng-app="App">
    <div ng-controller="MainCtrl">
      <input type="text"  ng-model="inputValue" input-restrictor>
      <input type="submit" ng-click="getdata()" />
    </div>
  </body>

</html>

Upvotes: 1

David Tao
David Tao

Reputation: 513

What I got from your issue are :

  1. only accept numbers, replace everything else with ''
  2. keep 2 decimal digits
  3. filter user's input and reflect/modify the value to the input element as user types

When binding the input value with ng-model, actually there are two main values behind the scenes internally: $viewValue and $modelValue. And a simple relationship between them is like this:

input DOM value -> $viewValue -> $modelValue

The $parsers only work between $viewValue and $modelValue. So the problem is when you insert a new parser into the $parsers, the modified value will only affect the $modleValue as you simply return a new value in the parse function.

$parsers are :

Array of functions to execute, as a pipeline, whenever the control reads value from the DOM. The functions are called in array order, each passing its return value through to the next. The last return value is forwarded to the $validators collection

more on : https://docs.angularjs.org/api/ng/type/ngModel.NgModelController#$parsers

And the returned value in the parser function WON'T affect the $viewValue so what you need to do is to also change that value and reflect on the input DOM element.

Code example:

app.directive('format', ['$filter', function ($filter) {
    return {
        require: 'ngModel',

        link: function (scope, elem, attrs, ctrl) {
            if (!ctrl) return;

            ctrl.$parsers.unshift(function (viewValue) {
                var plainNumber = viewValue.replace(/[^\d|\-+]/g, '');
                //use angular internal 'number' filter
                plainNumber = $filter('number')(plainNumber/100,2);

                //update the $viewValue
                ctrl.$setViewValue(plainNumber);
                //reflect on the DOM element
                ctrl.$render();
                //return the modified value to next parser
                return plainNumber;
            });
        }
    };
}]);

A working plnkr is here: http://plnkr.co/edit/aoBWhJhlJLiEhBm5hRNX?p=preview

At last, the reason why when you type 6th digit and it disappear is:

As you type 6 digits, the browser will convert and format the number you type to this kind of format: '1,234.56' (pay attention to the COMMA ',', the separator may defer on different browsers) , and you will see a warning given by angular in the console :

The specified value "1,234.56" is not a valid number. The value must match to the following regular expression: -?(\d+|\d+.\d+|.\d+)([eE][-+]?\d+)?

which is caused by the restriction from the type='number' on the input element, angular will perform extra internal validation to match the rule defined by angular itself.

Upvotes: 1

Related Questions