113

After looking for examples of how set focus elements with angular, I saw that most of them use some variable to watch for then set focus, and most of them use one different variable for each field they want to set focus. In a form, with a lot of fields, that implies in a lot of different variables.

With jquery way in mind, but wanting to do that in angular way, I made a solution that we set focus in any function using the element's id, so, as I am very new in angular, I'd like to get some opinions if that way is right, have problems, whatever, anything that could help me do this the better way in angular.

Basically, I create a directive that watch a scope value defined by the user with directive, or the default's focusElement, and when that value is the same as the element's id, that element set focus itself.

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

An input that needs to get focus by some reason, will do this way

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

Here any element to set focus:

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

And the example function that set focus:

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

Is that a good solution in angular? Does it have problems that with my poor experience I don't see yet?

sudo bangbang
  • 19,198
  • 7
  • 64
  • 71
Tiago Zortéa De Conto
  • 1,725
  • 3
  • 14
  • 17

6 Answers6

173

The problem with your solution is that it does not work well when tied down to other directives that creates a new scope, e.g. ng-repeat. A better solution would be to simply create a service function that enables you to focus elements imperatively within your controllers or to focus elements declaratively in the html.

DEMO

JAVASCRIPT

Service

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

Directive

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

Controller

.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
  • 27,539
  • 9
  • 57
  • 70
  • I really like this solution. Can you explain, however, the reason for using $timeout a bit more? Is the reason you used it due to an "Angular Thing" or "DOM Thing"? – user1821052 Nov 26 '14 at 19:48
  • It makes sure that it runs after any digest cycles that angular does, but this excludes digest cycles that are affected after an asynchronous action that is executed after the timeout. – ryeballar Nov 27 '14 at 04:49
  • 3
    Thanks! For those wondering where this is referenced in the angular docs, here's the [link](https://docs.angularjs.org/error/$rootScope/inprog) (took me forever to find) – user1821052 Dec 03 '14 at 20:00
  • @ryeballar, Thanks!. Nice simple solution. Just a question though. Can I use the factory that is created via attribute instead of waiting for some event to happen? – Pratik Gaikwad Jun 19 '15 at 16:48
  • @PratikGaikwad can you provide a use case? You're question is quite vague. State an example. – ryeballar Jun 20 '15 at 04:26
  • @ryeballar, The html example you provided is like. What I am thinking is like this: i.e adding it as an attribute to the textbox directly. – Pratik Gaikwad Jun 20 '15 at 04:43
  • Then how would you trigger a focus with that use case?, With the declarative html provided, it would have been a click event or any event that can be triggered from a button. But what would be trigger in the case you provided? – ryeballar Jun 20 '15 at 05:13
  • Only thing works is this ! Kudos. i wanted something i can use to focus on radio buttons, with hotkeys. This thing only worked. TY. – Gaurav Gandhi Nov 06 '15 at 10:18
  • 4
    It's insane the amount of work that's necessary in angular just to focus an input. – Bruno Santos May 01 '18 at 12:27
  • Is accessing the DOM in a Service the Angular way of doing things? Shouldn't that be restricted to directives? – David Casillas Sep 28 '18 at 10:23
19

About this solution, we could just create a directive and attach it to the DOM element that has to get the focus when a given condition is satisfied. By following this approach we avoid coupling controller to DOM element ID's.

Sample code directive:

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

A possible usage

.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
  • 1,668
  • 14
  • 20
  • 1
    what will happen when the `myCondition` $scope variable has been set to true already and then the user chooses to focus to another element, can you still retrigger the focus when `myCondition` is already true, your code watches the changes for the attribute `focusOnCondition` but it will not trigger when the value you try to change is still the same. – ryeballar Dec 09 '14 at 19:09
  • I'm going to update the sample, in our case we have two radio buttons, and we toggle the flag to true or false depending on the value, you could just change the myCondition flag to true or false – Braulio Dec 10 '14 at 09:30
  • Seems like a generic solution. Better than depending on ids. I like it. – mortb Mar 18 '16 at 12:11
  • In case anyone else tries this and it doesn't work, I had to change element.focus(); to element[0].focus(); – Adrian Carr Jul 22 '16 at 14:11
  • 1
    This solution is much more 'angular way' than id-based hack above. – setec Jul 27 '16 at 14:54
11

I like to avoid DOM lookups, watches, and global emitters whenever possible, so I use a more direct approach. Use a directive to assign a simple function that focuses on the directive element. Then call that function wherever needed within the scope of the controller.

Here's a simplified approach for attaching it to scope. See the full snippet for handling controller-as syntax.

Directive:

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

and in html:

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

or in the controller:

$scope.focusOnSaveInput();

angular.module('app', [])
  .directive('inputFocusFunction', function() {
    'use strict';
    return {
      restrict: 'A',
      link: function(scope, element, attr) {
        // Parse the attribute to accomodate assignment to an object
        var parseObj = attr.inputFocusFunction.split('.');
        var attachTo = scope;
        for (var i = 0; i < parseObj.length - 1; i++) {
          attachTo = attachTo[parseObj[i]];
        }
        // assign it to a function that focuses on the decorated element
        attachTo[parseObj[parseObj.length - 1]] = function() {
          element[0].focus();
        };
      }
    };
  })
  .controller('main', function() {});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.3/angular.min.js"></script>

<body ng-app="app" ng-controller="main as vm">
  <input input-focus-function="vm.focusOnSaveInput" ng-model="saveName">
  <button ng-click="vm.focusOnSaveInput()">Focus</button>
</body>

Edited to provide more explanation about the reason for this approach and to extend the code snippet for controller-as use.

cstricklan
  • 648
  • 4
  • 13
  • That's very nice, and has been working well for me. But now I have a set of inputs using `ng-repeat`, and I only want to set the focus function for the first one. Any idea how I could conditionally set a focus function for `` based on `$index` for example? – Garret Wilson Jan 26 '18 at 22:08
  • Glad it's useful. My angular 1 is a little rusty, but you should be able to add another attribute to the input, like `assign-focus-function-if="{{$index===0}}"`, and then as the first line of the directive exit early before assigning a function if that is not true: `if (attr.assignFocusFunctionIf===false) return;` Note I'm checking if it's explicitly `false` and not just falsey so the directive will still work if that attribute isn't defined. – cstricklan Jan 27 '18 at 23:03
  • Controller-as is much simplier with lodash. `_.set(scope, attributes.focusOnSaveInput, function() { element.focus(); })`. – Atomosk Sep 20 '18 at 08:22
9

You can try

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

for eg.

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

its working for me.

Anoop
  • 207
  • 3
  • 2
  • 4
    Note: This would only work if using full jQuery rather than relying on jqLite embedded in Angular. See https://docs.angularjs.org/api/ng/function/angular.element – John Rix Apr 01 '16 at 15:58
  • 4
    This is the jQuery way of doing this, not an angular way. The question specifically asks for how to do it in an angular way. – forgivenson Apr 12 '16 at 11:21
4

Another option would be to use Angular's built-in pub-sub architecture in order to notify your directive to focus. Similar to the other approaches, but it's then not directly tied to a property, and is instead listening in on it's scope for a particular key.

Directive:

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

Controller:

//Assume this is within your controller
//And you've hit the point where you want to focus the input:
$scope.$broadcast("focusTextInput");
Mattygabe
  • 1,713
  • 4
  • 22
  • 42
3

I prefered to use an expression. This lets me do stuff like focus on a button when a field is valid, reaches a certain length, and of course after load.

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

On a complex form this also reduces need to create additional scope variables for the purposes of focusing.

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

Community
  • 1
  • 1
winry
  • 191
  • 1
  • 10