Defina o foco do elemento de maneira angular

113

Depois de procurar exemplos de como definir elementos de foco com angular, vi que a maioria deles usa alguma variável para observar e definir o foco, e a maioria deles usa uma variável diferente para cada campo em que deseja definir o foco. Em um formulário, com muitos campos, isso implica em muitas variáveis ​​diferentes.

Com o jquery way em mente, mas querendo fazer isso de forma angular, fiz uma solução que colocamos o foco em qualquer função usando o id do elemento, então, como sou muito novo em angular, gostaria de obter algumas opiniões se desse jeito é certo, tenho problemas, enfim, qualquer coisa que pudesse me ajudar a fazer isso da melhor forma no angular.

Basicamente, eu crio uma diretiva que observa um valor de escopo definido pelo usuário com a diretiva, ou o focusElement padrão, e quando esse valor é o mesmo que o id do elemento, esse elemento define o próprio foco.

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

Uma entrada que precisa de foco por algum motivo, fará desta forma

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

Aqui, qualquer elemento para definir o foco:

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

E a função de exemplo que define o foco:

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

Essa é uma boa solução em angular? Ele tem problemas que eu ainda não vejo com minha experiência ruim?

Tiago Zortéa De Conto
fonte

Respostas:

173

O problema com sua solução é que ela não funciona bem quando ligada a outras diretivas que criam um novo escopo, por exemplo ng-repeat. Uma solução melhor seria simplesmente criar uma função de serviço que permita focar elementos de forma imperativa dentro de seus controladores ou focar elementos declarativamente no html.

DEMO

JAVASCRIPT

Serviço

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

Diretriz

  .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>
Ryeballar
fonte
Eu realmente gosto dessa solução. Você pode explicar, no entanto, a razão de usar $ timeout um pouco mais? O motivo pelo qual você o usou é uma "Coisa angular" ou "Coisa DOM"?
user1821052
Ele garante que seja executado após todos os ciclos de resumo que o Angular faz, mas isso exclui os ciclos de resumo que são afetados após uma ação assíncrona executada após o tempo limite.
Ryeballar
3
Obrigado! Para aqueles que estão se perguntando onde isso é referenciado nos documentos do angular, aqui está o link (demorei uma eternidade para encontrar)
user1821052
@ryeballar, obrigado !. Boa solução simples. Só uma pergunta. Posso usar a fábrica que é criada via atributo ao invés de esperar que algum evento aconteça?
Pratik Gaikwad
4
É insano a quantidade de trabalho necessária no angular apenas para focar uma entrada.
Bruno Santos
19

Sobre esta solução, poderíamos apenas criar uma diretiva e anexá-la ao elemento DOM que deve obter o foco quando uma determinada condição for satisfeita. Seguindo essa abordagem, evitamos acoplar o controlador aos IDs do elemento DOM.

Diretiva de código de amostra:

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

Um uso possível

.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
fonte
1
o que acontecerá quando a myConditionvariável $ scope já foi definida como true e então o usuário escolhe focar em outro elemento, você ainda pode reativar o foco quando myConditionjá for true, seu código observa as mudanças para o atributo, focusOnConditionmas não irá disparar quando o valor que você tenta alterar ainda é o mesmo.
Ryeballar
Vou atualizar a amostra, no nosso caso, temos dois botões de opção e alternamos o sinalizador para verdadeiro ou falso dependendo do valor, você pode apenas alterar o sinalizador myCondition para verdadeiro ou falso
Braulio
Parece uma solução genérica. Melhor do que depender de ids. Eu gosto disso.
mortb
Caso alguém tente fazer isso e não funcione, eu tive que mudar element.focus (); para o elemento [0] .focus ();
Adrian Carr
1
Esta solução é muito mais 'angular' do que o hack acima baseado em id.
setec
11

Gosto de evitar pesquisas DOM, relógios e emissores globais sempre que possível, então uso uma abordagem mais direta. Use uma diretiva para atribuir uma função simples que se concentra no elemento de diretiva. Em seguida, chame essa função sempre que necessário dentro do escopo do controlador.

Aqui está uma abordagem simplificada para anexá-lo ao escopo. Veja o snippet completo para lidar com a sintaxe do controlador como.

Diretriz:

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

e em html:

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

ou no controlador:

$scope.focusOnSaveInput();

Editado para fornecer mais explicações sobre o motivo dessa abordagem e para estender o trecho de código para uso pelo controlador.

cstricklan
fonte
Isso é muito bom e tem funcionado bem para mim. Mas agora eu tenho um conjunto de entradas usando ng-repeate só quero definir a função de foco para o primeiro. Alguma ideia de como eu poderia definir condicionalmente uma função de foco com <input>base em, $indexpor exemplo?
Garret Wilson
Ainda bem que é útil. Meu angular 1 está um pouco enferrujado, mas você deve ser capaz de adicionar outro atributo à entrada, como assign-focus-function-if="{{$index===0}}", e então, como a primeira linha da diretiva, saia antes de atribuir uma função se isso não for verdade: if (attr.assignFocusFunctionIf===false) return; Observe que estou verificando se é explicitamente falsee não apenas falsey, então a diretiva ainda funcionará se esse atributo não for definido.
cstricklan
Controller-as é muito mais simples com lodash. _.set(scope, attributes.focusOnSaveInput, function() { element.focus(); }).
Atomosk de
9

Podes tentar

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

por exemplo.

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

está funcionando para mim.

Anoop
fonte
4
Nota: Isso só funcionaria se usar jQuery completo em vez de depender do jqLite embutido no Angular. Consulte docs.angularjs.org/api/ng/function/angular.element
John Rix de
4
Esta é a maneira jQuery de fazer isso, não uma maneira angular. A questão pergunta especificamente como fazer isso de forma angular.
perdoado em
4

Outra opção seria usar a arquitetura pub-sub integrada do Angular para notificar sua diretiva ao foco. Semelhante às outras abordagens, mas não está diretamente vinculado a uma propriedade e, em vez disso, está ouvindo seu escopo para uma chave específica.

Diretriz:

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
fonte
3

Eu preferi usar uma expressão. Isso me permite fazer coisas como focar em um botão quando um campo é válido, atinge um determinado comprimento e, claro, após o carregamento.

<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">

Em um formulário complexo, isso também reduz a necessidade de criar variáveis ​​de escopo adicionais para fins de foco.

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

winry
fonte