maaartinus maaartinus - 3 months ago 8x
jQuery Question

Preserving cursor position with angularjs

The following snippet does what I want to an

, 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;
this.setSelectionRange(start, end);

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

The problem is that the
model sees the value before the change gets applied. I tried to Google for how to use
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.)


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

        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