Сохранение положения курсора с помощью угловых

Следующий фрагмент делает то, что я хочу для input, т.е. удаляет все небуквенные символы, преобразует их в верхний регистр и сохраняет позицию курсора.

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);
});

Я разместил этот фрагмент в link директивы, и он работает... в основном.

Проблема заключается в том, что модель angular видит значение до того, как будет применено изменение. Я попытался использовать Google для использования $apply или $digest или что-то здесь, но ничего не получилось.

(На самом деле, я каким-то образом справился с этим, но потом содержимое было повторно отображено, и я потерял позицию. Я не могу воспроизвести его, но это было недостаточно.)

Ответ 1

Способ сделать это, когда

  • Ввод очищается только один раз
  • ngChange на входе затем запускается только один раз

должен использовать массив $parsers, который предоставляет ngModelController. Он предназначен как место для воздействия на значение модели (через его возвращаемое значение), но также может использоваться как слушатель для ввода событий.

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;
      });
    }
  }
});

Однако я не уверен, что это использование $parsers - это немного взлома. Директива может использоваться как:

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

или если вы хотите использовать функцию ngChange:

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

Это можно увидеть в действии в http://plnkr.co/edit/dAJ46XmmC49wqTgdp2qz?p=preview

Ответ 2

Основные вещи, которые необходимы:

  • Требовать ngModelController, чтобы иметь возможность вызывать его методы и получать/устанавливать его значения. В частности...

  • Замените вызов element.val(y) на

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

    Я думаю, что я должен признать, что я не совсем уверен в внутренней работе ngModelController, чтобы понять, почему это необходимо.

  • Необязательно, но получение существующего значения в представлении element.val() можно сделать следующим образом:

    ngModelController.$viewValue;
    

    который по меньшей мере более согласуется с способом установки значения представления.

  • Опять необязательно, но прослушивание события input делает интерфейс немного приятнее, поскольку он, похоже, немного срабатывает перед событием keyup, поэтому вы не получаете вспышку необработанного ввода.

  • Добавление в массив $parsers для обработки ввода, похоже, останавливает любые обратные вызовы ngChange, которые запускаются для не обработанной версии ввода.

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

Объединяя все это как пользовательскую директиву:

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);
      });
    }
  }
});

который можно использовать как:

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

или если вы хотите использовать функцию ngChange:

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

и рассматривается в действии на http://plnkr.co/edit/FymZ8QEKwj2xXTmaExrH?p=preview

Изменить: добавьте часть в массив $parsers. Должен признаться, это был ответ @Engineer, который заставлял меня думать об этом.