12

The following snippet does what I want to an input, i.e., it removes all non-alphanumerical characters, converts to uppercase, and preserves the cursor position.

element = $(element);

element.keyup(function() {
    var x = element.val();
    var y = x && x.toUpperCase().replace(/[^A-Z\d]/g, '');
    if (x===y) return;
    var start = this.selectionStart;
    var end = this.selectionEnd + y.length - x.length;
    element.val(y);
    this.setSelectionRange(start, end);
});

I placed this snippet in the link of a directive and it works.... mostly.

The problem is that the angular model sees the value before the change gets applied. I tried to Google for how to use $apply or $digest or whatever here, but nothing worked.

(Actually, I somehow managed it, but then the content was re-rendered and I lost the position. I can't reproduce it, but it wasn't good enough, anyway.)

2 Answers 2

21
+50

A way of doing this where

  • The input is only cleaned once
  • ngChange on the input is then only fired once

is to use the $parsers array that the ngModelController provides. It's designed as a place to affect the model value (via its return value), but it also can be used as a listener to input events.

app.directive('cleanInput', function() {
  return {
    require: 'ngModel',
    link: function(scope, element, attrs, ngModelController) {
      var el = element[0];

      function clean(x) {
        return x && x.toUpperCase().replace(/[^A-Z\d]/g, '');
      }

      ngModelController.$parsers.push(function(val) {
        var cleaned = clean(val);

        // Avoid infinite loop of $setViewValue <-> $parsers
        if (cleaned === val) return val;

        var start = el.selectionStart;
        var end = el.selectionEnd + cleaned.length - val.length;

        // element.val(cleaned) does not behave with
        // repeated invalid elements
        ngModelController.$setViewValue(cleaned);
        ngModelController.$render();

        el.setSelectionRange(start, end);
        return cleaned;
      });
    }
  }
});

However, I'm not sure if this usage of $parsers is a bit of a hack. The directive can be used as:

<input type="text" clean-input ng-model="name">

or if you would like an ngChange function:

<input type="text" clean-input ng-model="name" ng-change="onChange()">

This can be seen in-action at http://plnkr.co/edit/dAJ46XmmC49wqTgdp2qz?p=preview

Sign up to request clarification or add additional context in comments.

2 Comments

great solution, thanks! all the other ways I tried to set the caret position caused the caret jumping back and forth, and your solution works just perfect
@michal-charemza It is possible to maintain different modelValue and viewValue. For example, I want to format input on the fly like XXX-XXX-XXXX but the model should contains XXXXXXXXXX only.
1

The main things that are needed are:

  • Require ngModelController to be able to call its methods and get/set its values. Specifically...

  • Replace the call element.val(y) with

    ngModelController.$setViewValue(y);
    ngModelController.$render();
    

    I think the I should admit, I'm not entirely sure on the inner workings of ngModelController to understand why this is necessary.

  • Optional, but getting the existing value in the view by element.val() can be instead done by:

    ngModelController.$viewValue;
    

    which is at least a more consistent with the way of setting the view value.

  • Again optional, but listening to the input event makes the interface a bit nicer, as it seems to fire a bit before the keyup event, so you don't get a flash of the unprocessed input.

  • Adding to the $parsers array to process the input seems to stop any ngChange callbacks being fired for the un-processed version of the input.

    ngModelController.$parsers.push(function(val) {
      // Return the processed value
    })
    

Putting all this together as a custom directive:

app.directive('cleanInput', function() {
  return {
    require: 'ngModel',
    link: function(scope, element, attrs, ngModelController) {
      function clean(x) {
        return x && x.toUpperCase().replace(/[^A-Z\d]/g, '');
      }

      ngModelController.$parsers.push(function(val) {
        return clean(val);
      })

      element.on('input', function() {
        var x = ngModelController.$viewValue;
        var y = clean(x);

        var start = this.selectionStart;
        var end = this.selectionEnd + y.length - x.length;

        ngModelController.$setViewValue(y);
        ngModelController.$render();
        this.setSelectionRange(start, end);
      });
    }
  }
});

which can be used as:

<input type="text" clean-input ng-model="name">

or if you would like an ngChange function:

<input type="text" clean-input ng-model="name ng-change="onChange()">

and seen in action at http://plnkr.co/edit/FymZ8QEKwj2xXTmaExrH?p=preview

Edit: add the part about $parsers array. I should admit, it was @Engineer's answer that made me think of it.

2 Comments

This seems to work fine, it just doesn't feel right: I need to clean it twice, once in oninput and once in $parsers.
I agree about not feeling right. Trying to convince myself/yourself that it's ok, I realised ngModelController controls/interacts with 2 things. The model value, and the view value. So then cleaning input once for each could be seen as ok. However, I have now thought of an alternative where it's cleaned once, but not sure if it's a bit of a back. Will post as a separate answer.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.