Validar campos após o usuário ter deixado um campo

92

Com o AngularJS, posso usar ng-pristineou ng-dirtypara detectar se o usuário entrou no campo. No entanto, desejo fazer a validação do lado do cliente somente depois que o usuário tiver deixado a área de campo. Isso ocorre porque quando um usuário insere um campo como e-mail ou telefone, ele sempre receberá um erro até que tenha concluído a digitação de seu e-mail completo, e esta não é uma experiência de usuário ideal.

Exemplo


ATUALIZAR:

O Angular agora vem com um evento de desfoque personalizado: https://docs.angularjs.org/api/ng/directive/ngBlur

mcranston18
fonte
3
Eu acho que isso é algo que angularjs deveria ter suportado ...
csg
Talvez. A validação de formulário embutida funciona muito bem que não sei se gostaria que ela fosse embutida. Afinal, a maioria dos casos de uso que desejo que o Angular valide imediatamente. ou seja, em um campo de número, quero que o formulário seja invalidado imediatamente se o usuário começar a digitar letras.
mcranston18 de
11
validação em desfoque é comum desde 15 anos ...
Olivvv

Respostas:

75

O Angular 1.3 agora tem opções de modelo de ng, e você pode definir a opção para, { 'updateOn': 'blur'}por exemplo, e você pode até debounce, quando o uso está digitando muito rápido, ou você deseja salvar algumas operações DOM caras (como a escrita de um modelo para vários locais DOM e você não quer um ciclo de $ digest acontecendo a cada tecla pressionada)

https://docs.angularjs.org/guide/forms#custom-triggers e https://docs.angularjs.org/guide/forms#non-immediate-debumped-model-updates

Por padrão, qualquer alteração no conteúdo acionará uma atualização do modelo e validação do formulário. Você pode substituir esse comportamento usando a diretiva ngModelOptions para vincular apenas a uma lista de eventos especificada. Ie ng-model-options = "{updateOn: 'blur'}" irá atualizar e validar somente depois que o controle perder o foco. Você pode definir vários eventos usando uma lista delimitada por espaço. Ie ng-model-options = "{updateOn: 'mousedown blur'}"

E debounce

Você pode atrasar a atualização / validação do modelo usando a chave debounce com a diretiva ngModelOptions. Esse atraso também se aplica a analisadores, validadores e sinalizadores de modelo como $ dirty ou $ pristine.

Ie ng-model-options = "{debounce: 500}" vai esperar meio segundo desde a última mudança de conteúdo antes de acionar a atualização do modelo e validação do formulário.

pocesar
fonte
2
Na maioria dos casos, isso não resolve o problema, porque você provavelmente ainda deseja receber ng-changeeventos antes de o usuário sair do campo. Por exemplo, no meu caso, solicito novos dados de minha API assim que o campo de entrada for válido, mas não quero mostrar uma mensagem de erro assim que for inválido - quero fazer isso apenas quando for inválido e o evento de desfoque ocorreu.
Tom
@Tom estou testando 1.3.0 rc-3e ele não dispara changeaté blur, verifique isto: plnkr.co/edit/RivVU3r7xfHO1hUybqMu?p=preview
Gill Bates
Essa deveria ser a melhor resposta aceita. Por que adicionar uma diretiva, quando você pode adicionar uma única propriedade?
Dan Hulton
92

A partir da versão 1.3.0, isso pode ser feito facilmente com $touched, o que é verdade depois que o usuário sai do campo.

<p ng-show="form.field.$touched && form.field.$invalid">Error</p>

https://docs.angularjs.org/api/ng/type/ngModel.NgModelController

user1506145
fonte
1
Isso ainda não executaria a validação após cada key-up? OP afirmou que queria fazer a validação somente após o usuário ter deixado o campo.
vir
@comecme sim, seria, o ng-model-options padrão é o updateOn: 'default'que significa, para entradas,
tecle
5
A grande falha aqui, porém, é que você não terá a mesma experiência de validação se voltar ao campo. Você está de volta à estaca zero, onde valida a cada pressionamento de tecla, uma vez que já está definido como tocado. Estou surpreso que tenha recebido tantos votos positivos.
dudewad
3
@dudewad isso está correto, mas acho que é o comportamento desejado em termos de UX - se você voltar para um campo inválido, a mensagem de erro deve persistir até que o valor seja válido, não até que você digite a primeira letra.
danbars de
@dudewad Não ajudaria o usuário a validar em cada pressionamento depois de já saber que um campo era inválido depois de preenchê-lo pela primeira vez? Assim que souberem que o campo é inválido, é provável que queiram saber rapidamente quando o campo é válido e validar a cada pressionamento de tecla aceleraria esse processo, talvez?
Alan Dunning,
32

Resolvi isso expandindo o que @jlmcdonald sugeriu. Eu criei uma diretiva que seria aplicada automaticamente a todos os elementos de entrada e seleção:

var blurFocusDirective = function () {
    return {
        restrict: 'E',
        require: '?ngModel',
        link: function (scope, elm, attr, ctrl) {
            if (!ctrl) {
                return;
            }

            elm.on('focus', function () {
                elm.addClass('has-focus');

                scope.$apply(function () {
                    ctrl.hasFocus = true;
                });
            });

            elm.on('blur', function () {
                elm.removeClass('has-focus');
                elm.addClass('has-visited');

                scope.$apply(function () {
                    ctrl.hasFocus = false;
                    ctrl.hasVisited = true;
                });
            });

            elm.closest('form').on('submit', function () {
                elm.addClass('has-visited');

                scope.$apply(function () {
                    ctrl.hasFocus = false;
                    ctrl.hasVisited = true;
                });
            });

        }
    };
};

app.directive('input', blurFocusDirective);
app.directive('select', blurFocusDirective);

Isso adicionará has-focuse has-visitedclasses a vários elementos conforme o usuário foca / visita os elementos. Você pode então adicionar essas classes às suas regras CSS para mostrar erros de validação:

input.has-visited.ng-invalid:not(.has-focus) {
    background-color: #ffeeee;   
}

Isso funciona bem porque os elementos ainda obtêm $ propriedades inválidas etc., mas o CSS pode ser usado para dar ao usuário uma experiência melhor.

lambinador
fonte
2
Por que '? NgModel' é necessário em vez de simplesmente 'ngModel'? Eu entendo que é necessário que o ngModel esteja lá, mas por que o ponto de interrogação?
Noremac
2
Obrigado pela ideia. Devo apontar que requer jQuery (eu acho - não consegui ver um .closest () em jql).
iandotkelly
1
Nem todos os elementos <input> são suportados pelo ngModel (por exemplo, input type = submit). O ? requer ngModel apenas em elementos que o suportam.
chmanie
5
Para ser honesto, você não deve se desviar da maneira angular de definir os estados do campo de entrada como formName.inputName.$dirty. Eu sugeriria editar a diretiva para escrever um booleano semelhante formName.inputName.$focused, em vez de usar nomes de classe para desfoque e booleanos para validação.
Tom
17

Consegui fazer isso com um pouco de CSS bem simples. Isso exige que as mensagens de erro sejam irmãs da entrada a que se relacionam e que tenham uma classe da qual error.

:focus ~ .error {
    display:none;
}

Depois de atender a esses dois requisitos, isso ocultará qualquer mensagem de erro relacionada a um campo de entrada focado, algo que acho que o angularjs deveria fazer de qualquer maneira. Parece um descuido.

Nicholas
fonte
2
Ahh. Esta é uma maneira inteligente de ir. Eu prefiro isso a escrever javascript para fazer isso.
deck
5
A desvantagem desta abordagem é que quando alguém está corrigindo um erro, o erro não é mais visível, o que não é o ideal. O ideal seria que o erro não apareça até desfocar, mas não desapareça até que seja corrigido.
Chris Nicola
4

Isso parece ser implementado como padrão nas versões mais recentes do angular.

As classes ng-untouched e ng -oked são definidas respectivamente antes e depois que o usuário teve o foco em um elemento validado.

CSS

input.ng-touched.ng-invalid {
   border-color: red;
}
Arg0n
fonte
4

Com relação à solução de @lambinator ... Eu estava recebendo o seguinte erro no angular.js 1.2.4:

Erro: [$ rootScope: inprog] $ digest já em andamento

Não tenho certeza se fiz algo errado ou se isso é uma alteração no Angular, mas remover as scope.$applyinstruções resolveu o problema e as classes / estados ainda estão sendo atualizados.

Se você também estiver vendo esse erro, tente o seguinte:

var blurFocusDirective = function () {
  return {
    restrict: 'E',
    require: '?ngModel',
    link: function (scope, elm, attr, ctrl) {
      if (!ctrl) {
        return;
      }
      elm.on('focus', function () {
        elm.addClass('has-focus');
        ctrl.$hasFocus = true;
      });

      elm.on('blur', function () {
        elm.removeClass('has-focus');
        elm.addClass('has-visited');
        ctrl.$hasFocus = false;
        ctrl.$hasVisited = true;
      });

      elm.closest('form').on('submit', function () {
        elm.addClass('has-visited');

        scope.$apply(function () {
          ctrl.hasFocus = false;
          ctrl.hasVisited = true;
        });
      });
    }
  };
};
app.directive('input', blurFocusDirective);
app.directive('select', blurFocusDirective);
melcher
fonte
2

Pode funcionar para você escrever uma diretiva personalizada que envolve o método blur () de javascript (e executa uma função de validação quando acionada); há um problema do Angular que tem um exemplo (bem como uma diretiva genérica que pode vincular a outros eventos não suportados nativamente pelo Angular):

https://github.com/angular/angular.js/issues/1277

Se você não quiser seguir esse caminho, sua outra opção seria configurar $ watch no campo, novamente acionando a validação quando o campo for preenchido.

jlmcdonald
fonte
2

Para aprender mais sobre as respostas fornecidas, você pode simplificar a marcação de entrada usando pseudo classes CSS3 e apenas marcando os campos visitados com uma classe para atrasar a exibição de erros de validação até que o usuário perca o foco no campo:

(O exemplo requer jQuery)

JavaScript

module = angular.module('app.directives', []);
module.directive('lateValidateForm', function () {
    return {
        restrict: 'AC',
        link: function (scope, element, attrs) {
            $inputs = element.find('input, select, textarea');

            $inputs.on('blur', function () {
                $(this).addClass('has-visited');
            });

            element.on('submit', function () {
                $inputs.addClass('has-visited');
            });
        }
    };
});

CSS

input.has-visited:not(:focus):required:invalid,
textarea.has-visited:not(:focus):required:invalid,
select.has-visited:not(:focus):required:invalid {
  color: #b94a48;
  border-color: #ee5f5b;
}

HTML

<form late-validate-form name="userForm">
  <input type="email" name="email" required />
</form>
Patrick Glandien
fonte
2

baseado na resposta do @nicolas. CSS puro deve funcionar, ele só mostrará a mensagem de erro no desfoque

<input type="email" id="input-email" required
               placeholder="Email address" class="form-control" name="email"
               ng-model="userData.email">
        <p ng-show="form.email.$error.email" class="bg-danger">This is not a valid email.</p>

CSS

.ng-invalid:focus ~ .bg-danger {
     display:none;
}
keithics
fonte
Isso é bom, mas a mensagem de erro desaparecerá quando o usuário clicar novamente no campo, o que não é o ideal.
Bennett McElwee
2

Aqui está um exemplo usando ng-messages (disponível no angular 1.3) e uma diretiva personalizada.

A mensagem de validação é exibida em desfoque na primeira vez que o usuário deixa o campo de entrada, mas quando ele corrige o valor, a mensagem de validação é removida imediatamente (não está mais desfocada).

JavaScript

myApp.directive("validateOnBlur", [function() {
    var ddo = {
        restrict: "A",
        require: "ngModel",
        scope: {},
        link: function(scope, element, attrs, modelCtrl) {
            element.on('blur', function () {
                modelCtrl.$showValidationMessage = modelCtrl.$dirty;
                scope.$apply();
            });
        }
    };
    return ddo;
}]);

HTML

<form name="person">
    <input type="text" ng-model="item.firstName" name="firstName" 
        ng-minlength="3" ng-maxlength="20" validate-on-blur required />
    <div ng-show="person.firstName.$showValidationMessage" ng-messages="person.firstName.$error">
        <span ng-message="required">name is required</span>
        <span ng-message="minlength">name is too short</span>
        <span ng-message="maxlength">name is too long</span>
    </div>
</form>

PS. Não se esqueça de baixar e incluir ngMessages em seu módulo:

var myApp = angular.module('myApp', ['ngMessages']);
jurgemar
fonte
1

ng-model-options no AngularJS 1.3 (beta no momento desta escrita) está documentado para suportar {updateOn: 'blur'}. Para versões anteriores, algo como o seguinte funcionou para mim:

myApp.directive('myForm', function() {
  return {
    require: 'form',
    link: function(scope, element, attrs, formController) {
      scope.validate = function(name) {
        formController[name].isInvalid
            = formController[name].$invalid;
      };
    }
  };
});

Com um modelo como este:

<form name="myForm" novalidate="novalidate" data-my-form="">
<input type="email" name="eMail" required="required" ng-blur="validate('eMail')" />
<span ng-show="myForm.eMail.isInvalid">Please enter a valid e-mail address.</span>
<button type="submit">Submit Form</button>
</form>
Terence Bandoian
fonte
1

Use o estado do campo $ Touch O campo foi tocado para isso, conforme mostrado no exemplo abaixo.

<div ng-show="formName.firstName.$touched && formName.firstName.$error.required">
    You must enter a value
</div>
Kushal Sharma
fonte
0

Você pode definir dinamicamente a classe css has-error (supondo que você esteja usando bootstrap) usando ng-class e uma propriedade no escopo do controlador associado:

plunkr: http://plnkr.co/edit/HYDlaTNThZE02VqXrUCH?p=info

HTML:

<div ng-class="{'has-error': badEmailAddress}">
    <input type="email" class="form-control" id="email" name="email"
        ng-model="email" 
        ng-blur="emailBlurred(email.$valid)">
</div>

Controlador:

$scope.badEmailAddress = false;

$scope.emailBlurred = function (isValid) {
    $scope.badEmailAddress = !isValid;
};
Mike
fonte
0

Se você usar bootstrap 3 e lesscss, poderá ativar a validação de desfoque com o seguinte menos snippet:

:focus ~ .form-control-feedback.glyphicon-ok {
  display:none;
}

:focus ~ .form-control-feedback.glyphicon-remove {
  display:none;
}

.has-feedback > :focus {
  & {
    .form-control-focus();
  }
}
Stefan
fonte
0

outI usei uma diretiva. Aqui está o código:

app.directive('onBlurVal', function () {
    return {
        restrict: 'A',
        link: function (scope, element, attrs, controller) {

            element.on('focus', function () {
                element.next().removeClass('has-visited');
                element.next().addClass('has-focus');
            });

            element.on('blur', function () {

                element.next().removeClass('has-focus');
                element.next().addClass('has-visited');
            });
        }
    }
})

Todo o meu controle de entrada tem um elemento span como o próximo elemento, que é onde minha mensagem de validação é exibida e, portanto, a diretiva como um atributo é adicionada a cada controle de entrada.

Eu também tenho (opcional) as classes css .has-focus e has-Visit em meu arquivo css, que você vê sendo referenciado na diretiva.

NOTA: lembre-se de adicionar 'on-blur-val' exatamente desta forma ao seu controle de entrada sem os apóstrofos

user3676608
fonte
0

Ao usar o ng-focus, você pode atingir seu objetivo. você precisa fornecer ng-focus em seu campo de entrada. E enquanto escreve seus derivados ng-show, você também tem que escrever uma lógica diferente. Como o código abaixo:

<input type="text" class="form-control" name="inputPhone" ng-model="demo.phoneNumber" required ng-focus> <div ng-show="demoForm.inputPhone.$dirty && demoForm.inputPhone.$invalid && !demoForm.inputPhone.$focused"></div>

Raio
fonte
0

Podemos usar as funções onfocus e onblur. Seria simples e melhor.

<body ng-app="formExample">
  <div ng-controller="ExampleController">
  <form novalidate class="css-form">
    Name: <input type="text" ng-model="user.name" ng-focus="onFocusName='focusOn'" ng-blur="onFocusName=''" ng-class="onFocusName" required /><br />
    E-mail: <input type="email" ng-model="user.email" ng-focus="onFocusEmail='focusOn'" ng-blur="onFocusEmail=''" ng-class="onFocusEmail" required /><br />
  </form>
</div>

<style type="text/css">
 .css-form input.ng-invalid.ng-touched {
    border: 1px solid #FF0000;
    background:#FF0000;
   }
 .css-form input.focusOn.ng-invalid {
    border: 1px solid #000000;
    background:#FFFFFF;
 }
</style>

Experimente aqui:

http://plnkr.co/edit/NKCmyru3knQiShFZ96tp?p=preview

Naveen Kumar
fonte