Definir o foco na primeira input inválida no formulário AngularJs

Eu li vários artigos e perguntas StackOverflow relativas à configuração de foco em AngularJs.

Infelizmente, todos os exemplos lidos assumem que há algum atributo que posso adicionar ao elemento para obter foco, por exemplo, uma diretiva focusMe .

No entanto, e se eu não souber de antemão qual input para definir o foco? Em particular, como faço para definir o foco para o primeiro elemento de input em um formulário que tenha $ invalid set – isto é, um elemento que falha na validação. Pode haver várias inputs que falham na validação, portanto, não posso usar uma diretiva que tente chamar .focus () com base nisso. (Estou fazendo isso por razões de Acessibilidade / WCAG, é uma boa prática fazê-lo no envio de cliques para minimizar os pressionamentos de tecla para localizar o primeiro campo com falha na validação).

O object $ error fornecerá todos os controles que falharam na validação, mas eles são agrupados pelo tipo de falha que não está em ordem de aparição no formulário.

Tenho certeza de que posso chegar a uma maneira simplificada de fazer isso. Uma diretiva no formulário, que recebe alguma transmissão quando o foco precisa ser definido – essa diretiva pode, então, procurar o primeiro elemento $ invalid. No entanto, isso parece muito complexo e eu gostaria de saber se essa é uma maneira melhor e mais “angular” de fazer isso.

Ok, então a resposta foi mais simples do que eu pensava.

Tudo o que eu precisava era de uma diretiva para colocar o formulário em si, com um manipulador de events procurando o evento de envio. Isso pode, então, percorrer o DOM procurando o primeiro elemento que tenha a class .ng-invalid nele.

Exemplo usando jQLite:

myApp.directive('accessibleForm', function () { return { restrict: 'A', link: function (scope, elem) { // set up event handler on the form element elem.on('submit', function () { // find the first invalid element var firstInvalid = elem[0].querySelector('.ng-invalid'); // if we find one, set focus if (firstInvalid) { firstInvalid.focus(); } }); } }; }); 

O exemplo aqui usa uma diretiva Attribute, você pode expandir o exemplo para ter uma diretiva de elemento (restrict: ‘E’) e include um template que converta isso para a. Esta é, no entanto, uma preferência pessoal.

Você pode criar diretivas como algumas outras respostas ou, alternativamente, você pode conectá-las com o ng-submit e implementar a lógica no controlador.

Visão:

 

Controlador:

 $scope.save = function(yourForm) { if (!yourForm.$valid) { angular.element("[name='" + yourForm.$name + "']").find('.ng-invalid:visible:first').focus(); return false; } }; 

Você também pode usar angular.element

 angular.element('input.ng-invalid').first().focus(); 

Visão

 

Controlador

 $scope.myAction= function(isValid) { if (isValid) { //You can place your ajax call/http request here } else { angular.element('input.ng-invalid').first().focus(); } }; 

ngMessages usado para validação

O jeito não jquery

 angular.element($document[0].querySelector('input.ng-invalid')).focus(); 

Ao usar esse método, é necessário passar $document como parâmetro no seu controlador angular

 angular.module('myModule') .controller('myController', ['$document', '$scope', function($document, $scope){ // Code Here }]); 

Você pode usar o jQuery puro para selecionar a primeira input inválida:

$('input.ng-invalid').first().focus();

  .directive('accessibleForm', function () { return { restrict: 'A', link: function (scope, elem) { // set up event handler on the form element elem.on('submit', function () { // find the first invalid element var firstInvalid = elem[0].querySelector('.ng-invalid'); if (firstInvalid && firstInvalid.tagName.toLowerCase() === 'ng-form') { firstInvalid = firstInvalid.querySelector('.ng-invalid'); } // if we find one, set focus if (firstInvalid) { firstInvalid.focus(); } }); } }; }) 

Eu tenho jogado com essa ideia por um tempo e eu criei minha própria solução, isso pode ajudar pessoas que são adversas a rastrear o DOM, como eu.

Tanto quanto eu posso dizer que os elementos de formulário se registram em uma ordem consistente (ou seja, de cima para baixo) e seus nomes e estados de validação estão disponíveis no escopo através do nome do formulário (por exemplo, $ scope.myForm).

Isso me levou a pensar que havia uma maneira de encontrar a primeira input de formulário inválida sem rastrear o DOM e, em vez disso, rastrear as estruturas internas do js angular. Abaixo está a minha solução, mas pressupõe que você tem outra maneira de focar elementos de formulário, estou transmitindo para uma diretiva personalizada, se a transmissão corresponder ao nome do elemento em que ele se concentrará (o que é útil em si controle qual elemento tem foco na primeira carga).

A function para encontrar o primeiro inválido (idealmente compartilhado para os controladores através de um serviço)

 function findFirstInvalid(form){ for(var key in form){ if(key.indexOf("$") !== 0){ if(form[key].$invalid){ return key; } } } } 

E a diretiva de foco personalizado

 directives.directive('focus', function($timeout){ return { require: 'ngModel', restrict: 'A', link: function(scope, elem, attrs, ctrl){ scope.$on('inputFocus', function(e, name){ if(attrs.name === name){ elem.focus(); } }); } } }); 

Fiz algumas pequenas modificações na ótima solução escrita por iandotkelly. Essa solução adiciona uma animação que é acionada na rolagem e faz um foco no elemento selecionado depois disso.

 myApp.directive('accessibleForm', function () { return { restrict: 'A', link: function (scope, elem) { // set up event handler on the form element elem.on('submit', function () { // find the first invalid element var firstInvalid = elem[0].querySelector('.ng-invalid'); // if we find one, we scroll with animation and then we set focus if (firstInvalid) { angular.element('html:not(:animated),body:not(:animated)') .animate({ scrollTop: angular.element(firstInvalid).parent().offset().top }, 350, 'easeOutCubic', function () { firstInvalid.focus(); }); } }); } }; }); 

apenas uma linha:

 if($scope.formName.$valid){ //submit } else{ $scope.formName.$error.required[0].$$element.focus(); } 

Você pode adicionar um atributo em cada elemento do formulário, que é uma function (idealmente, uma diretiva) que recebe um id de campo. Esse id de campo teria que se correlacionar de alguma forma ao seu object $ error. A function pode verificar se o id está em seu object $ error e, em caso afirmativo, retornar a configuração do atributo para um erro.

  

Se você tivesse um erro, isso geraria isso.

  

Você pode usar isso para definir seu estilo e agora você sabe quais campos têm erros. Infelizmente você não sabe qual é o primeiro campo.

Uma solução seria usar o jQuery e o primeiro filtro. Se você seguir esse caminho, confira http://docs.angularjs.org/api/angular.element

Outra solução seria adicionar em seus campos de formulário um parâmetro de ordem de campo para a function: {{errorCheck (‘name’, 1)}}. Você poderia enviar os nomes dos campos de erro para uma matriz e, em seguida, classificá-los pelo parâmetro de ordem de campo. Isso poderia lhe dar mais flexibilidade.

Espero que isto ajude.

Eu fui inspirado pelo chaojidan acima para sugerir essa variação para aqueles que estão usando formas aninhadas angulares de 1.5.9 ng:

 class FormFocusOnErr implements ng.IDirective { static directiveId: string = 'formFocusOnErr'; restrict: string = "A"; link = (scope: ng.IScope, elem, attrs) => { // set up event handler on the form element elem.on('submit', function () { // find the first invalid element var firstInvalid = angular.element( elem[0].querySelector('.ng-invalid'))[0]; // if we find one, set focus if (firstInvalid) { firstInvalid.focus(); // ng-invalid appears on ng-forms as well as // the inputs that are responsible for the errors. // In such cases, the focus will probably fail // because we usually put the ng-focus attribute on divs // and divs don't support the focus method if (firstInvalid.tagName.toLowerCase() === 'ng-form' || firstInvalid.hasAttribute('ng-form') || firstInvalid.hasAttribute('data-ng-form')) { // Let's try to put a finer point on it by selecting // the first visible input, select or textarea // that has the ng-invalid CSS class var firstVisibleInvalidFormInput = angular.element(firstInvalid.querySelector("input.ng-invalid,select.ng-invalid,textarea.ng-invalid")).filter(":visible")[0]; if (firstVisibleInvalidFormInput) { firstVisibleInvalidFormInput.focus(); } } } }); } } // Register in angular app app.directive(FormFocusOnErr.directiveId, () => new FormFocusOnErr()); 

Isso porque o focus() não é suportado no jqLite e nos documentos angulares no elemento.

Um método não baseado em diretivas poderia se parecer com isso. É o que eu usei, desde que eu tenho um botão ‘next’ na parte inferior de cada página que está realmente no index.html no rodapé. Eu uso esse código em main.js.

 if (!$scope.yourformname.$valid) { // find the invalid elements var visibleInvalids = angular.element.find('.ng-invalid:visible'); if (angular.isDefined(visibleInvalids)){ // if we find one, set focus visibleInvalids[0].focus(); } return; }