4

I coded an angular directive for inhibiting typing from inputs by specifying a regex. In that directive I indicate a regex that will be used for allow the input data. Conceptually, it works fine, but there are two bugs in this solution:

  1. In the first Plunker example the input must allow only numbers or numbers followed by a dot [.], or numbers followed by a dot followed by numbers with no more than four digits.
    • If I type a value '1.1111' and after that I go to the first digit and so type another digit (in order to get a value as '11.1111') , nothing happening. The bug is in the fact I use the expression elem.val() + event.key on my regex validator. I do not know how to get the whole current value for a input on a keypress event;
  2. The second one is the fact that some characters (grave, acute, tilde, circumflex) are being allowed on typing (press one of them more than once), althought the regex does not allow them.

What changes do I need to make in my code in order to get an effective type restriction by regex?

  <html ng-app="app">    
  <head>
    <script data-require="angularjs@1.6.4" data-semver="1.6.4" src="https://code.angularjs.org/1.6.4/angular.min.js"></script>
    <link rel="stylesheet" href="style.css" />
    <script src="script.js"></script>
  </head>

  <body>
    <h1>Restrict typing by RegExp</h1>
    PATTERN 1 (^\d+$|^\d+[.]$|^\d+[.]\d{1,4}$) <input type="text" allow-typing="^\d+$|^\d+[.]$|^\d+[.]\d{1,4}$"/><br>
    ONLY NUMBERS <input type="text" allow-typing="^[0-9]+$"/><br>
    ONLY STRINGS <input type="text" allow-typing="^[a-zA-Z]+$"/>
  </body>

</html>

Directive

angular.module('app', []).directive('allowTyping', function() {
  return {
    restrict: 'A',
    link: function(scope, elem, attrs, ctrl) {
      var regex = attrs.allowTyping;
      elem.bind('keypress', function(event) {
        var input = elem.val() + event.key;
        var validator = new RegExp(regex);
        if(!validator.test(input)) {
          event.preventDefault();
          return false;
        }
      });
    }
  };
});
GalAbra
  • 5,048
  • 4
  • 23
  • 42
Geison Santos
  • 187
  • 1
  • 3
  • 16
  • 2
    Your regex should be either `^\d+(?:\.(?:\d{1,4})?)?$` or `^(?:\d+(?:\.(?:\d{1,4})?)?)?$` which is a firmly specific pattern. If it is allowing anything else, something else is wrong. –  Jan 13 '18 at 00:13
  • What is ?: in your regex? – Geison Santos Jan 13 '18 at 01:12
  • @GeisonSantos `(?: ... )` is a non-capturing group. It matches without capturing everything enclosed. – ctwheels Jan 15 '18 at 20:59
  • i think you should bind to event onkeyup instead of onkeypress, so you do not need do elem.val() + event.key any more. this will fix your 1th bug – Shen Yudong Jan 16 '18 at 12:55
  • @yudongshen, I tried on this way (using onkeyup). Using this approach I would need to replace incorrect characters using the same directive regex, but in a reverse way. Do you have idea of how to do that? – Geison Santos Jan 16 '18 at 13:21
  • @GeisonSantos, save value while onkeypress, and roll back to it if valid failure on event onkeyup. how about this way? – Shen Yudong Jan 16 '18 at 13:29
  • Btw, if you change it to `^((?:\d+(?:\.(?:\d{1,4})?)?)?).*$`, after a valid input char event (but before display), you could just write back the correction `$1` to the box. Thus, it never shows wrong data and has the illusion of disallowing invalid chars, or ones in the wrong place. –  Jan 17 '18 at 17:09

4 Answers4

1

If this were my code, I'd change tactics entirely: I would listen for input events instead of trying to micromanage the user interactions with the field.

The approach you are taking, in general, has problems. The biggest one is that keypress won't be emitted for all changes to the field. Notably,

  1. It is not triggered by DELETE and BACKSPACE keys.

  2. Input methods can bypass it. When you entered diacritics as diacritics, your code was not registering the change. In general, if the user is using an input method, there is no guarantee that each new character added to the field will result in a keypress event. It depends on the method the user has chosen.

  3. keypress does not help when the user cuts from the field or pastes into the field.

You could add code to try to handle all the cases above, but it would get complex quick. You've already run into an issue with elem.val() + event.key because the keypress may not always be about a character inserted at the end of the field. The user may have moved the caret so you have to keep track of caret position. One comment suggested listening to keyup but that does not help with input methods or paste/cut events.

In contrast, the input event is generated when the value of the field changes, as the changes occur. All cases above are taken care of. This, for instance, would work:

elem.bind('input', function(event) {
  var validator = new RegExp(regex);
  elem.css("background-color", !validator.test(elem.val()) ? "red" : null);
});

This is a minimal illustration that you could plop into your fiddle to replace your current event handler. In a real application, I'd give the user a verbose error message rather than just change the color of the field and I'd create validator just once, outside the event handler, but this gives you the idea.

(There's also a change event but you do no want to use that. For text fields, it is generated when the focus leaves the field, which is much too late.)

Louis
  • 146,715
  • 28
  • 274
  • 320
0
  1. You specified 4 different patterns 3 different pattens in your regex separated by an alteration sign: ^\d+$|^\d+[.]$|^\d+[.]\d{1,4}$ - this will not fulfill the criteria of input must allow only numbers followed by a dot [.], followed by a number with no more than four digits. The bug "where nothing happens" occurs because the variable you are checking against is not what you think it is, check the screenshot on how you can inspect it, and what it is:

enter image description here

  1. Can not reproduce.
Mindaugas Bernatavičius
  • 3,757
  • 4
  • 31
  • 58
  • Mindaugas, I'm sorry. My bad... the first regex, as you see, accepts numbers, or numbers + dot or numbers + dot + numbers. I will correct the question description. For you get the second bug, you need to use the combination of space + accent (grave, tilde, circumflex). – Geison Santos Jan 13 '18 at 00:05
  • Ok, then the regex is usable (could be improved). Did you understand why the first bug is present (for the 11.1111 string)? You are using element data + the key sent on event as input - so it is always added to the end, and not where you want it to be. For the 2nd - still can't reproduce, all works for me. Can you provide steps to reproduce which would identify the field and the precise sequence of actions, like it is normally done in the software development world? – Mindaugas Bernatavičius Jan 13 '18 at 00:27
  • "Did you understand why the first bug is present (for the 11.1111 string)? You are using element data + the key sent on event as input - so it is always added to the end, and not where you want it to be." Yes, I knew. I would like to use another approach to get input data... Some suggestion? – Geison Santos Jan 13 '18 at 00:39
  • 1
    You are using incorrect event handler. The value entered in the field `elem.val()` keypress and keydown is not there, so you try to append it to the end, which can be easily broken. A simple fix would be to use events that happen when the input is there, like `keyup`. Or you can filter on a per-character basis - meaning only allow certain characters and check them one-by-one and then validate the pattern at the end (when the user leaves the field, or presses submit). This is how it is usually done. – Mindaugas Bernatavičius Jan 13 '18 at 08:52
0

See Plnkr Fixed as per your approach:

The explanation of why and the changes are explained below. Side note: I would not implement it this way (use ngModel with $parsers and $formatters, e.g. https://stackoverflow.com/a/15090867/2103767) - implementing that is beyond the scope of your question. However I found a full implementation by regexValidate by Ben Lesh which will fit your problem domain:-

If I type a value '1.1111' and after that I go to the first digit and so type another digit (in order to get a value as '11.1111') , nothing happening.

because in your code below

var input = elem.val() + event.key;

you are assuming that the event.key is always appended at the end.

So how to get the position of the correct position and validate the the reconstructed string ? You can use an undocumented event.target.selectionStart property. Note even though you are not selecting anything you will have this populated (IE 11 and other browsers). See Plnkr Fixed

The second one is the fact that some characters (grave, acute, tilde, circumflex) are being allowed on typing (press one of them more than once), althought the regex does not allow them.

Fixed the regex - correct one below:

^[0-9]*(?:\.[0-9]{0,4})?$

So the whole thing looks as below

  link: function(scope, elem, attrs, ctrl) {
    var regex = attrs.allowTyping;
    elem.bind('keypress', function(event) {
      var pos = event.target.selectionStart;
      var oldViewValue = elem.val();
      var input = newViewValue(oldViewValue, pos, event.key);
      console.log(input);
      var validator = new RegExp(regex);
      if (!validator.test(input)) {
        event.preventDefault();
        return false;
      }
    });

    function newViewValue(oldViewValue, pos, key) {
      if (!oldViewValue) return key;
      return [oldViewValue.slice(0, pos), key, oldViewValue.slice(pos)].join('');
    }
  }
bhantol
  • 9,368
  • 7
  • 44
  • 81
0

You can change the event to keyup, so the test would run after every additional character is added.

It means you need to save the last valid input, so if the user tries to insert a character that'll turn the string invalid, the test will restore the last valid value.

Hence, the updated directive:

angular.module('app', [])
    .directive('allowTyping', function()  {
        return {
            restrict : 'A',
            link : function(scope, elem, attrs, ctrl) {
                var regex = attrs.allowTyping;
                var lastInputValue = "";
                elem.bind('keyup', function(event) {
                var input = elem.val();
                var validator = new RegExp(regex);

                if (!validator.test(input))
                    // Restore last valid input
                    elem.val(lastInputValue).trigger('input');
                else
                    // Update last valid input
                    lastInputValue = input;
                });
            }
        };
    });
GalAbra
  • 5,048
  • 4
  • 23
  • 42