Atrasar validações em AngularJS. Minha vez.

Recentemente, um cliente estava precisando de algum comportamento personalizado para quando mostrar o estado inválido de um campo / modelo ng-invalid. O cliente é uma grande empresa (estilo empresarial), então a equipe ditou como o aplicativo deveria se comportar. Não havia espaço para discussão, infelizmente.

O que eles precisavam fazer era:

  • Não mostre o estado inválido (através ng-invalid) ao digitar (por exemplo, um endereço de e-mail), mas depois que o campo perder o foco
  • Se o estado inválido já for mostrado, a correção de erros deve levar imediatamente ao estado válido ( ng-valid)

Depois de ouvir o primeiro ponto, meu primeiro pensamento foi mudar o CSS, para que as alterações do CSS para ng-invalidfossem “revertidas” quando o campo estivesse atualmente em foco. Depois de ouvir o segundo ponto, precisamos ter uma ideia diferente.

Uma primeira ideia

Alguém propôs imitar o comportamento das validações que foi usado no projeto, mas disparar em blur em vez de adicionar $parser/ $formatter. Ainda não havia resposta sobre como resolver o segundo ponto, mas copiar os comportamentos de todas as validações utilizadas não era opção para mim . Simplesmente não faz sentido copiar um código eficaz e, basicamente, implementar a mesma coisa duas vezes.

Além disso, quando fui contratado para o projeto, recomendei fazer muito mais validação no front end, portanto, poder simplesmente adicionar mais validadores (mesmo personalizados) deve ser facilmente possível, sem ter que pensar em quando disparar: todos os validadores deve ser definido como $parserse $formattersde ngModel, que é a forma recomendada. Isso deve ser tudo.

O que eu inventei

Então minha ideia era: toda validação acontece por meio de funções $parsere $formatter, que são adicionadas por meio de diretivas. Talvez eu pudesse controlar quando eles estão em vigor e quando não? Em geral, tudo que preciso é uma maneira de entrar em ação depois que todas as diretivas tiverem sido compiladas e vinculadas, e remover e adicionar as funções já definidas no momento certo.

Então comecei a prototipar e testar e o resultado foi esta diretiva:

angular.module('myApp')
.directive('rbDelayValidation', function() {
return {
priority
: 9999,
require
: 'ngModel',
link
: function(scope, element, attr, ctrl) {
var parsers = [],
formatters
= [],
hasParsersFormatters
= true,
oldViewValue
;

removeParsersFormatters
();

element
.on('blur', function() {
var unwatch;

if (oldViewValue !== ctrl.$viewValue) {
oldViewValue
= ctrl.$viewValue;

injectParsersFormatters
();
ctrl
.$setViewValue(ctrl.$viewValue);

if (ctrl.$valid) {
removeParsersFormatters
();
} else {
unwatch
= scope.$watch(
function() { return ctrl.$valid; },
function(newValue, oldValue) {
if (newValue !== oldValue) {
removeParsersFormatters
();
unwatch
();
}
}
);
}
}
});

function removeParsersFormatters() {
if (hasParsersFormatters) {
while (ctrl.$parsers.length > 0) {
parsers
.push(ctrl.$parsers.shift());
}

while (ctrl.$formatters.length > 0) {
formatters
.push(ctrl.$formatters.shift());
}

hasParsersFormatters
= !hasParsersFormatters;
}
}

function injectParsersFormatters() {
if (!hasParsersFormatters) {
while (parsers.length > 0) {
ctrl
.$parsers.push(parsers.shift());
}

while (formatters.length > 0) {
ctrl
.$formatters.push(formatters.shift());
}

hasParsersFormatters
= !hasParsersFormatters;
}
}
}
}
});

O que isso está fazendo?

Basicamente, ele remove todas as funções $parsere $formatterdo modelo e as adiciona novamente após um desfoque quando uma mudança no $viewValuefoi detectada. Em seguida, ele configura o $viewValuenovamente, o que é meio feio, mas a única maneira que encontrei de acionar as funções readicionadas por meio do AngularJS (alguma outra ideia?) Se o modelo se tornar válido, está tudo bem e voltamos ao estado inicial (= remover todas as funções novamente). Mas se não for válido, ainda precisamos esperar por mais mudanças até que o modelo se torne válido e, em seguida, retornar ao estado inicial.

Com esse comportamento, essa diretriz auxilia no atendimento às demandas do cliente. Além disso, este comportamento seria melhor no que diz respeito à usabilidade, pois não alerta o usuário durante a digitação, mesmo antes de chegar “ao fim” de sua entrada. (Na verdade, IMHO, seria ótimo quando o formulário respondesse com algum tipo de informação “ainda não chegamos lá”, por exemplo, com uma borda laranja em vez de vermelha ou nenhuma; mas esse não era o requisito aqui.)

Como usá-lo

Basta adicioná-lo a um campo com alguns outros atributos de diretiva ou diretivas:

<input ng-model="foo.bar" name="foobar" ng-minlength="10" rb-delay-validation>

Por causa da prioridade da diretiva rbDelayValidation, todos $parsere $formatterjá devem estar definidos. Se você estiver escrevendo diretivas personalizadas, certifique-se de que elas tenham uma prioridade definida para algo abaixo de 9999 ou altere a diretiva rbDelayValidation :-).

O que fazer melhor

Esta solução não é a melhor, mas é a única que encontrei que se adapta às demandas e ao ambiente do meu cliente. Eu ficaria feliz em saber uma maneira de iniciar a chamada de todos $parsere $formatterfunções através do AngularJS sem redefinir o $viewValue, porque ele dispara tudo $viewChangeListenernovamente.

Além disso, gostaria de ouvir qualquer crítica ou risco que possa ter perdido. Eu aprecio quaisquer pensamentos e ideias!

Coisas que também podem ser adicionadas:

  • apenas remova / adicione analisadores, de modo que as alterações externas do modelo sejam imediatamente refletidas pela classe /ng-validng-invalid
  • adicione um estado “não estamos lá ainda”, para que o usuário saiba que a entrada ainda não está pronta (útil para números IBAN ou entrada específica de sintaxe diferente).

Obrigado :-).

PS: Tudo fica melhor com AngularJS 1.3 e ngModelOptions 😉