Change focus to input on a key event in AngularJS

All we need is an easy explanation of the problem, so here it is.

Test case: http://jsbin.com/ahugeg/4/edit (Slightly long)

In the above test case, I have three input elements, generated by ng-repeat directive. My intention in this test case, is that hitting the up/down arrows in one of these inputs should move focus to the input in the corresponding direction, if there is an input available in that direction.

I am new to AngularJS, so I might be missing on some straightforward way to do this. Anyway, I defined two new directives (on-up and on-down), to handle the up and down events and am calling the $scope.focusNext and $scope.focusPrev methods, passing the correct entry, relative to which the focus should move. This is where I am stuck.

I know it is not the angular-way to deal with DOM elements in controllers, but I can’t see how the focus can be seen as an attribute/property of a model. I even thought of having a separate $scope.focusedEntry, but then should I watch for changes on that property? Even if I do and I detect changes, how can I access the input element corresponding to the entry I want focused?

Any help on how this should be done are very much appreciated.

How to solve :

I know you bored from this bug, So we are here to help you! Take a deep breath and look at the explanation of your problem. We have many solutions to this problem, But we recommend you to use the first method because it is tested & true method that will 100% work for you.

Method 1

I just wrote this up and tested it briefly – it does what you want without all the extra clutter in your controller and in the HTML. See it working here.

HTML:

<body ng-controller="Ctrl">
    <input ng-repeat="entry in entries" value="{{entry}}" key-focus />
</body>

Controller:

function Ctrl($scope) {
  $scope.entries = [ 'apple', 'ball', 'cow' ];
}

Directive:

app.directive('keyFocus', function() {
  return {
    restrict: 'A',
    link: function(scope, elem, attrs) {
      elem.bind('keyup', function (e) {
        // up arrow
        if (e.keyCode == 38) {
          if(!scope.$first) {
            elem[0].previousElementSibling.focus();
          }
        }
        // down arrow
        else if (e.keyCode == 40) {
          if(!scope.$last) {
            elem[0].nextElementSibling.focus();
          }
        }
      });
    }
  };
});

Method 2

I had a similar problem and used this simple directive. It works as ng-show and ng-hide would- only with focus, if it’s attribute resolves as true:

.directive('focusOn',function() {
    return {
        restrict : 'A', 
        link : function($scope,$element,$attr) {
            $scope.$watch($attr.focusOn,function(focusVal) {
                if(focusVal === true) {
                    setTimeout(function() {
                        $element.focus();
                    },50);
                }
            });
        }
    }
})

Method 3

Inspired by @Trevor’s solution, here’s what I settled on,

app.directive('focusIter', function () {

    return function (scope, elem, attrs) {
        var atomSelector = attrs.focusIter;

        elem.on('keyup', atomSelector, function (e) {
            var atoms = elem.find(atomSelector),
                toAtom = null;

            for (var i = atoms.length - 1; i >= 0; i--) {
                if (atoms[i] === e.target) {
                    if (e.keyCode === 38) {
                        toAtom = atoms[i - 1];
                    } else if (e.keyCode === 40) {
                        toAtom = atoms[i + 1];
                    }
                    break;
                }
            }

            if (toAtom) toAtom.focus();

        });

        elem.on('keydown', atomSelector, function (e) {
            if (e.keyCode === 38 || e.keyCode === 40)
                e.preventDefault();
        });

    };
});

This defines an attribute focus-iter to be set on the parent element of all the repeated inputs. See this in action here: http://jsbin.com/ahugeg/10/.

The advantage over @Trevor’s is that I can set an arbitrary selector for the value of focus-iter attribute to specify exactly which elements the focus jumping should work with. As a crazy example, try setting focus-iter attribute to input:even :). This helps since in my application, the inputs come with quite a bit of extra markup around them, unlike the test case.

Note: Use and implement method 1 because this method fully tested our system.
Thank you 🙂

All methods was sourced from stackoverflow.com or stackexchange.com, is licensed under cc by-sa 2.5, cc by-sa 3.0 and cc by-sa 4.0

Leave a Reply