Angular.js: função ngModelController $ parsers não chamada de forma consistente

Criado em 10 ago. 2015  ·  3Comentários  ·  Fonte: angular/angular.js

Ao impor valores de entrada para estar em conformidade com certas regras usando uma função ngModelController $ parser, a função $ parser não é chamada em pressionamentos de tecla "inválidos" repetidos, o que permite que caracteres inválidos sejam adicionados.

No exemplo abaixo, a diretiva destina-se a remover todos os caracteres não-dígitos. geralmente funciona, mas se você digitar o mesmo caractere não-dígito duas vezes seguidas (por exemplo, 'xx'), um único 'x' aparecerá na entrada. se você digitar 'x' uma terceira vez, o 'x' será removido.

Também incluí um caso de teste com falha.

todo o código está sendo executado em http://codepen.io/visnup/pen/YXgLVq?editors=101

uso diretivo

<div ng-app="digits">
  <input ng-model="number" placeholder="digits only" digits-only />
</div>

diretriz

angular
  .module('digits', [])
  .directive('digitsOnly', function() {
    return {
      require: 'ngModel',
      link: function link(scope, element, attrs, ngModel) {
        ngModel.$parsers.push(function(value) {
          var numbers = value.replace(/\D/g, '');
          element.val(numbers);
          return numbers;
        });
      }
    };
  });

caso de teste reprovado

describe('digits', function() {
  beforeEach(angular.mock.module('digits'));

  let scope, input;
  beforeEach(inject(function($compile, $rootScope) {
    scope = $rootScope;
    input = $compile('<input ng-model="number" digits-only />')(scope);
  }));

  // works
  it('should block non-digits', function() {
    input.val('111x');
    input.triggerHandler('input');
    expect(input.val()).to.equal('111');
  });

  // doesn't work
  it('should not flash incorrect input', function() {
    input.val('111x');
    input.triggerHandler('input');
    input.val('111x');
    input.triggerHandler('input');
    expect(input.val()).to.equal('111');
  });
});

Isso já foi mencionado em # 10700.

Comentários muito úteis

$parsers é para transformar (uma cópia de) $viewValue antes de "salvá-lo" como $modelValue (e é isso que seu exemplo está fazendo). Na verdade, ele não está mudando $viewValue (embora atualize o valor do elemento), o que leva a um comportamento inesperado.

Isso é melhor ilustrado com um exemplo:

  1. O usuário insere '1'.

    • _Before_ parsing:



      • $modelValue : indefinido


      • $viewValue : 1


      • element.val() : 1



    • _Após_ análise:



      • $modelValue : 1


      • $viewValue : 1


      • element.val() : 1



  2. O usuário insere 'x'.

    • _Before_ parsing:



      • $modelValue : 1


      • $viewValue : 1x


      • element.val() : 1x



    • _Após_ análise:



      • $modelValue : 1


      • $viewValue : 1x (nunca atualizado do analisador fn)


      • element.val() : 1 (atualizado do analisador fn)



  3. O usuário insere 'x'.

    • _Before_ parsing:



      • $modelValue : 1


      • $viewValue : 1x (Nenhuma alteração detectada, já que $viewValue também era 1x )


      • element.val() : 1x



    • _No_ análise (visto que nenhuma alteração foi detectada em $viewValue ) !!!



      • $modelValue : 1


      • $viewValue : 1x


      • element.val() : 1x



  4. O usuário insere 'x'.

    • _Before_ parsing:



      • $modelValue : 1


      • $viewValue : 1xx


      • element.val() : 1xx



    • _Após_ análise:



      • $modelValue : 1


      • $viewValue : 1xx


      • element.val() : 1



  5. _ (... voltar ao passo (2) ...) _

Existem várias maneiras "angulares" de lidar adequadamente com isso, por exemplo:

ngModel.$parsers.push(function (value) {
  var numbers = value.replace(/\D/g, '');
  if (numbers !== value) {
    ngModel.$setViewValue(numbers);   // Update the `$viewValue`
    ngModel.$render();                // Update the element's displayed value
  }
  return numbers;
});

Caneta atualizada


Estou encerrando, pois tudo parece funcionar conforme o esperado.
@visnup ,

Todos 3 comentários

$parsers é para transformar (uma cópia de) $viewValue antes de "salvá-lo" como $modelValue (e é isso que seu exemplo está fazendo). Na verdade, ele não está mudando $viewValue (embora atualize o valor do elemento), o que leva a um comportamento inesperado.

Isso é melhor ilustrado com um exemplo:

  1. O usuário insere '1'.

    • _Before_ parsing:



      • $modelValue : indefinido


      • $viewValue : 1


      • element.val() : 1



    • _Após_ análise:



      • $modelValue : 1


      • $viewValue : 1


      • element.val() : 1



  2. O usuário insere 'x'.

    • _Before_ parsing:



      • $modelValue : 1


      • $viewValue : 1x


      • element.val() : 1x



    • _Após_ análise:



      • $modelValue : 1


      • $viewValue : 1x (nunca atualizado do analisador fn)


      • element.val() : 1 (atualizado do analisador fn)



  3. O usuário insere 'x'.

    • _Before_ parsing:



      • $modelValue : 1


      • $viewValue : 1x (Nenhuma alteração detectada, já que $viewValue também era 1x )


      • element.val() : 1x



    • _No_ análise (visto que nenhuma alteração foi detectada em $viewValue ) !!!



      • $modelValue : 1


      • $viewValue : 1x


      • element.val() : 1x



  4. O usuário insere 'x'.

    • _Before_ parsing:



      • $modelValue : 1


      • $viewValue : 1xx


      • element.val() : 1xx



    • _Após_ análise:



      • $modelValue : 1


      • $viewValue : 1xx


      • element.val() : 1



  5. _ (... voltar ao passo (2) ...) _

Existem várias maneiras "angulares" de lidar adequadamente com isso, por exemplo:

ngModel.$parsers.push(function (value) {
  var numbers = value.replace(/\D/g, '');
  if (numbers !== value) {
    ngModel.$setViewValue(numbers);   // Update the `$viewValue`
    ngModel.$render();                // Update the element's displayed value
  }
  return numbers;
});

Caneta atualizada


Estou encerrando, pois tudo parece funcionar conforme o esperado.
@visnup ,

Esse problema ocorre com pressionamentos de tecla repetidos sempre que o novo $ viewValue não for confirmado. O uso sugerido de $ setViewValue na coleção $ parsers às vezes é evitado com a preferência de definir diretamente $ viewValue em alguns fóruns.

O seguinte também funcionaria:

ngModel.$viewValue = numbers;   // Update the '$viewValue'
ngModel.$commitViewValue(); //update $$lastCommittedViewValue
ngModel.$render();                // Update the element's displayed value

Se você quiser evitar quaisquer armadilhas de recursão, $$ lastCommittedViewValue pode ser atualizado diretamente se você não se importar em violar o encapsulamento. Infelizmente, quando a recursão infinita ocorre nessas funções, tenho visto casos em que ela não é relatada ao console, mesmo após o máximo de ocorrências. Vale a pena dedicar alguns minutos para colocar um ponto de interrupção durante o teste e verificar quaisquer recursões.

As explicações de @snaptech combinadas formam uma solução de ouro :): +1:

Esta página foi útil?
0 / 5 - 0 avaliações