Establecer el foco del elemento de forma angular

113

Después de buscar ejemplos de cómo establecer elementos de enfoque con angular, vi que la mayoría de ellos usa alguna variable para observar y luego establecer el enfoque, y la mayoría de ellos usa una variable diferente para cada campo en el que quieren establecer el enfoque. En una forma, con muchos campos, eso implica muchas variables diferentes.

Con jquery en mente, pero queriendo hacer eso de manera angular, hice una solución que establecemos el foco en cualquier función usando la identificación del elemento, así que, como soy muy nuevo en angular, me gustaría obtener algunas opiniones si de esa manera es correcto, tengo problemas, lo que sea, cualquier cosa que pueda ayudarme a hacer esto de la mejor manera en angular.

Básicamente, creo una directiva que observa un valor de alcance definido por el usuario con la directiva, o el focusElement predeterminado, y cuando ese valor es el mismo que el ID del elemento, ese elemento establece el foco en sí.

angular.module('appnamehere')
  .directive('myFocus', function () {
    return {
      restrict: 'A',
      link: function postLink(scope, element, attrs) {
        if (attrs.myFocus == "") {
          attrs.myFocus = "focusElement";
        }
        scope.$watch(attrs.myFocus, function(value) {
          if(value == attrs.id) {
            element[0].focus();
          }
        });
        element.on("blur", function() {
          scope[attrs.myFocus] = "";
          scope.$apply();
        })        
      }
    };
  });

Una entrada que necesita enfocarse por alguna razón, lo hará de esta manera

<input my-focus id="input1" type="text" />

Aquí cualquier elemento para establecer el foco:

<a href="" ng-click="clickButton()" >Set focus</a>

Y la función de ejemplo que establece el enfoque:

$scope.clickButton = function() {
    $scope.focusElement = "input1";
}

¿Es una buena solución en angular? ¿Tiene problemas que con mi mala experiencia todavía no veo?

Tiago Zortéa De Conto
fuente

Respuestas:

173

El problema con su solución es que no funciona bien cuando está vinculada a otras directivas que crean un nuevo alcance, por ejemplo ng-repeat. Una mejor solución sería simplemente crear una función de servicio que le permita enfocar elementos imperativamente dentro de sus controladores o enfocar elementos declarativamente en el html.

MANIFESTACIÓN

JAVASCRIPT

Servicio

 .factory('focus', function($timeout, $window) {
    return function(id) {
      // timeout makes sure that it is invoked after any other event has been triggered.
      // e.g. click events that need to run before the focus or
      // inputs elements that are in a disabled state but are enabled when those events
      // are triggered.
      $timeout(function() {
        var element = $window.document.getElementById(id);
        if(element)
          element.focus();
      });
    };
  });

Directiva

  .directive('eventFocus', function(focus) {
    return function(scope, elem, attr) {
      elem.on(attr.eventFocus, function() {
        focus(attr.eventFocusId);
      });

      // Removes bound events in the element itself
      // when the scope is destroyed
      scope.$on('$destroy', function() {
        elem.off(attr.eventFocus);
      });
    };
  });

Controlador

.controller('Ctrl', function($scope, focus) {
    $scope.doSomething = function() {
      // do something awesome
      focus('email');
    };
  });

HTML

<input type="email" id="email" class="form-control">
<button event-focus="click" event-focus-id="email">Declarative Focus</button>
<button ng-click="doSomething()">Imperative Focus</button>
centeno
fuente
Me gusta mucho esta solución. ¿Puede explicar, sin embargo, la razón por la que se usa $ timeout un poco más? ¿La razón por la que lo usó se debe a una "cosa angular" o una "cosa DOM"?
user1821052
Se asegura de que se ejecute después de cualquier ciclo de resumen que hace angular, pero esto excluye los ciclos de resumen que se ven afectados después de una acción asincrónica que se ejecuta después del tiempo de espera.
ryeballar
3
¡Gracias! Para aquellos que se preguntan dónde se hace referencia a esto en los documentos angulares, aquí está el enlace (me tomó una eternidad encontrarlo)
usuario1821052
@ryeballar, ¡Gracias !. Buena solución simple. Aunque solo una pregunta. ¿Puedo usar la fábrica que se crea mediante atributo en lugar de esperar a que suceda algún evento?
Pratik Gaikwad
4
Es una locura la cantidad de trabajo que se necesita en angular solo para enfocar una entrada.
Bruno Santos
19

Acerca de esta solución, podríamos simplemente crear una directiva y adjuntarla al elemento DOM que debe obtener el foco cuando se cumple una condición determinada. Al seguir este enfoque, evitamos acoplar el controlador a los ID de elementos DOM.

Directiva de código de muestra:

gbndirectives.directive('focusOnCondition', ['$timeout',
    function ($timeout) {
        var checkDirectivePrerequisites = function (attrs) {
          if (!attrs.focusOnCondition && attrs.focusOnCondition != "") {
                throw "FocusOnCondition missing attribute to evaluate";
          }
        }

        return {            
            restrict: "A",
            link: function (scope, element, attrs, ctrls) {
                checkDirectivePrerequisites(attrs);

                scope.$watch(attrs.focusOnCondition, function (currentValue, lastValue) {
                    if(currentValue == true) {
                        $timeout(function () {                                                
                            element.focus();
                        });
                    }
                });
            }
        };
    }
]);

Un posible uso

.controller('Ctrl', function($scope) {
   $scope.myCondition = false;
   // you can just add this to a radiobutton click value
   // or just watch for a value to change...
   $scope.doSomething = function(newMyConditionValue) {
       // do something awesome
       $scope.myCondition = newMyConditionValue;
  };

});

HTML

<input focus-on-condition="myCondition">
Braulio
fuente
1
¿Qué sucederá cuando la myConditionvariable $ scope ya se haya establecido en verdadero y luego el usuario elija enfocarse en otro elemento? ¿Puede volver a activar el enfoque cuando myConditionya es verdadero? Su código observa los cambios para el atributo focusOnConditionpero no se activará cuando el valor que intentas cambiar sigue siendo el mismo.
ryeballar
Voy a actualizar la muestra, en nuestro caso tenemos dos botones de radio, y cambiamos la bandera a verdadero o falso según el valor, simplemente puede cambiar la bandera myCondition a verdadero o falso
Braulio
Parece una solución genérica. Mejor que depender de identificadores. Me gusta.
mortb
En caso de que alguien más intente esto y no funcione, tuve que cambiar element.focus (); al elemento [0] .focus ();
Adrian Carr
1
Esta solución es mucho más 'angular' que el truco basado en id anterior.
setec
11

Me gusta evitar las búsquedas de DOM, los relojes y los emisores globales siempre que sea posible, así que utilizo un enfoque más directo. Utilice una directiva para asignar una función simple que se centre en el elemento directivo. Luego llame a esa función donde sea necesario dentro del alcance del controlador.

Aquí hay un enfoque simplificado para adjuntarlo al alcance. Vea el fragmento completo para manejar la sintaxis del controlador.

Directiva:

app.directive('inputFocusFunction', function () {
    'use strict';
    return {
        restrict: 'A',
        link: function (scope, element, attr) {
            scope[attr.inputFocusFunction] = function () {
                element[0].focus();
            };
        }
    };
});

y en html:

<input input-focus-function="focusOnSaveInput" ng-model="saveName">
<button ng-click="focusOnSaveInput()">Focus</button>

o en el controlador:

$scope.focusOnSaveInput();

Editado para proporcionar más explicaciones sobre el motivo de este enfoque y para ampliar el fragmento de código para su uso como controlador.

cstricklan
fuente
Eso es muy bueno y me ha funcionado bien. Pero ahora tengo un conjunto de entradas usando ng-repeat, y solo quiero configurar la función de enfoque para la primera. ¿Alguna idea de cómo podría establecer condicionalmente una función de enfoque <input>basada en, $indexpor ejemplo?
Garret Wilson
Me alegro de que sea útil. Mi angular 1 está un poco oxidado, pero debería poder agregar otro atributo a la entrada, como assign-focus-function-if="{{$index===0}}", y luego, como la primera línea de la directiva, salga antes de asignar una función si eso no es cierto: if (attr.assignFocusFunctionIf===false) return; tenga en cuenta que estoy verificando si es explícitamente falsey no solo falsey, por lo que la directiva seguirá funcionando si ese atributo no está definido.
cstricklan
Controller-as es mucho más simple con lodash. _.set(scope, attributes.focusOnSaveInput, function() { element.focus(); }).
Atomosk
9

Puedes probar

angular.element('#<elementId>').focus();

por ej.

angular.element('#txtUserId').focus();

está trabajando para mí.

Anoop
fuente
4
Nota: Esto solo funcionaría si usa jQuery completo en lugar de confiar en jqLite incrustado en Angular. Ver docs.angularjs.org/api/ng/function/angular.element
John Rix
4
Esta es la forma jQuery de hacer esto, no una forma angular. La pregunta pregunta específicamente cómo hacerlo de forma angular.
perdonanson
4

Otra opción sería usar la arquitectura pub-sub incorporada de Angular para notificar a su directiva que se enfoque. Similar a los otros enfoques, pero luego no está directamente vinculado a una propiedad, sino que está escuchando su alcance para una clave en particular.

Directiva:

angular.module("app").directive("focusOn", function($timeout) {
  return {
    restrict: "A",
    link: function(scope, element, attrs) {
      scope.$on(attrs.focusOn, function(e) {
        $timeout((function() {
          element[0].focus();
        }), 10);
      });
    }
  };
});

HTML:

<input type="text" name="text_input" ng-model="ctrl.model" focus-on="focusTextInput" />

Controlador:

//Assume this is within your controller
//And you've hit the point where you want to focus the input:
$scope.$broadcast("focusTextInput");
Mattygabe
fuente
3

Preferí usar una expresión. Esto me permite hacer cosas como centrarme en un botón cuando un campo es válido, alcanza una cierta longitud y, por supuesto, después de la carga.

<button type="button" moo-focus-expression="form.phone.$valid">
<button type="submit" moo-focus-expression="smsconfirm.length == 6">
<input type="text" moo-focus-expression="true">

En una forma compleja, esto también reduce la necesidad de crear variables de alcance adicionales con el fin de enfocar.

Ver https://stackoverflow.com/a/29963695/937997

winry
fuente