Establecer el foco en la primera entrada no válida en el formulario AngularJS


He leído varios artículos y preguntas de StackOverflow relacionadas con el establecimiento del enfoque en AngularJS.

Desafortunadamente, todos los ejemplos que he leído asumen que hay algún atributo que puedo agregar al elemento para ganar enfoque, por ejemplo, una directiva FocusMe.

Sin embargo, ¿qué pasa si no sé de antemano en qué entrada establecer el enfoque? En particular, ¿cómo establezco el foco en el primer elemento de entrada en un formulario que tiene $invalid set - es decir, un elemento que falla validación. Podría haber varias entradas que fallan en la validación, por lo que no puedo usar una directiva que solo intente llamar .focus() basado en esto. (Estoy haciendo esto por razones de Accesibilidad / WCAG, es una buena práctica hacerlo al hacer clic en enviar para minimizar las pulsaciones de teclas para encontrar el primer campo que ha fallado en la validación).

El objeto error error dará todos los controles que fallan en la validación, pero se agrupan por el tipo de error no en ningún orden de aparición en el formulario.

Estoy seguro que se me ocurre alguna forma de hacer esto. Una directiva en el formulario, que recibe algo de difusión cuando se necesita establecer el foco - esa directiva puede entonces buscar el primer elemento invalid inválido. Sin embargo, esto parece muy complejo y me gustaría saber si es una mejor forma más "angular" de hacer esto.

Author: Community, 2013-12-04

12 answers

Ok, entonces la respuesta fue más simple de lo que pensé.

Todo lo que necesitaba era una directiva para poner en el formulario en sí, con un controlador de eventos buscando el evento enviar. Esto puede atravesar el DOM buscando el primer elemento que tenga la clase. ng-invalid en él.

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

El ejemplo aquí usa una directiva de atributos, puede expandir el ejemplo para que sea una directiva de elementos (restrict: 'E') e incluir una plantilla que convierta esto en una . Sin embargo, esta es una preferencia personal.

 85
Author: iandotkelly,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-03-31 17:03:14

Puede crear la directiva como algunas otras respuestas o alternativamente puede engancharla con ng-submit e implementar la lógica en el controlador.

Vista:

<form name='yourForm' novalidate ng-submit="save(yourForm)">
</form>

Controlador:

$scope.save = function(yourForm) {
  if (!yourForm.$valid) {
    angular.element("[name='" + yourForm.$name + "']").find('.ng-invalid:visible:first').focus();
    return false;
  }
};
 14
Author: nnattawat,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-11-01 01:48:15

También puede usar angular.elemento

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

Ver

<form name="myForm" novalidate="novalidate" data-ng-submit="myAction(myForm.$valid)" autocomplete="off"></form>

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 usados para la validación

El camino sin jquery

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

Al usar este método, debe pasar $document como parámetro en su controlador angular

angular.module('myModule')
.controller('myController', ['$document', '$scope', function($document, $scope){
    // Code Here
}]);
 11
Author: Sajan Mullappally,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-03-17 05:46:09

Puede usar jQuery puro para seleccionar la primera entrada no válida:

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

 6
Author: Edmond Chui,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-04-02 05:28:46

    .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();
                    }
                });
            }
        };
    })
 4
Author: chaojidan,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-10-23 12:00:37

He estado jugando con esta idea durante un tiempo y se me ocurrió mi propia solución, puede ayudar a las personas que son adversas a gatear el DOM, como yo.

Por lo que puedo decir, los elementos del formulario se registran en un orden consistente (es decir, de arriba a abajo) y sus nombres y estados de validación están disponibles en el ámbito a través de lo que sea el nombre del formulario (por ejemplo, scope ámbito.myForm).

Esto me llevó a pensar que había una manera de encontrar la primera entrada de formulario no válida sin rastrear el DOM y en su lugar arrastrando las estructuras internas de angular js. A continuación está mi solución, pero asume que tiene alguna otra forma de enfocar los elementos de la forma, estoy transmitiendo a una directiva personalizada, si la emisión coincide con el nombre del elemento que se centrará en sí mismo (que es útil en sí mismo como se llega a controlar qué elemento se centra en la primera carga).

La función para encontrar el primer inválido (idealmente compartido a los controladores a través de un servicio)

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

Y el directiva de enfoque 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();
                }
            });
        }
    }
});
 2
Author: h.coates,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-02-20 04:59:41

Hice algunas pequeñas modificaciones a la gran solución escrita por iandotkelly. Esta solución agrega una animación que se activa al desplazarse y hace un foco en el elemento seleccionado después de eso.

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();
                        });
                }
            });
        }
    };
});
 1
Author: Mathemagician,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-10-26 08:45:34

Solo una línea:

if($scope.formName.$valid){
    //submit
}
else{
    $scope.formName.$error.required[0].$$element.focus();
}
 1
Author: sonphuong,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-10-27 07:50:40

Puede agregar un atributo en cada elemento del formulario que es una función (idealmente una directiva) que recibe un id de campo. Este id de campo tendría que correlacionarse de alguna manera con su objeto error error. La función puede comprobar si el id está en su objeto error error y, si es así, devolver la configuración del atributo para un error.

<input id="name" class="{{errorCheck('name')}}">

Si tuviera un error, generaría esto.

<input id="name" class="error">

Puede usar esto para establecer su estilo y ahora sabe qué campos tienen errores. Desafortunadamente no lo sabes que es el primer campo.

Una solución sería usar jQuery y el .primer filtro. Si vas por esta ruta, echa un vistazo a http://docs.angularjs.org/api/angular.element

Otra solución sería agregar a los campos de su formulario un parámetro de orden de campos para la función: {{errorCheck('name', 1)}}. Puede empujar los nombres de campo de error a una matriz, luego ordenarlos por el parámetro orden de campo. Esto podría darle más flexibilidad.

Espero que esto ayude.

 0
Author: Darryl,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-12-04 02:13:45

Me inspiré en chaojidan para sugerir esta variación para aquellos que están usando formas angulares anidadas 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());
 0
Author: CAK2,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-23 11:33:25

Esto se debe a que focus() no es compatible con jqLite ni con el elemento Angular docs on.

 0
Author: Acacio Martins,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-06-28 12:09:06

Un método no basado en directivas podría tener este aspecto. Es lo que usé, ya que tengo un botón 'siguiente' en la parte inferior de cada página que en realidad está en index.html en el pie de página. Yo uso este código en 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;
    }
 -1
Author: CarComp,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-06-26 15:25:59