set focus on a specific dynamically input on keydown

1,364 views
Skip to first unread message

Esa Yletyinen

unread,
Jan 11, 2017, 8:47:51 AM1/11/17
to Angular
I need to create a directive which sets focus on a specific (here, the next) dynamically input when a specific key (e.g. enter) is pressed. E.g.

<input ng-repeat="i = [0,1,2] enternextfocus='i'">

I found this directive in the code created by the previous programmer working on my project, but it doesn't work with dynamically created inputs:

app.directive('enternextfocus', function() {
    return {
        restrict: 'A',
        link: function(scope, elem, attr) {
            elem.bind('keydown', function(event) {
                var idx = scope.$eval(attr.enternextfocus);
                var code = event.keyCode || event.which;
                if (code === 13) {
                    var nextelem = document.querySelector('[enternextfocus="' + (idx + 1) + '"]');
                    if(nextelem !== null && nextelem !== undefined) {
                        event.preventDefault();
                        nextelem.focus();
                    }
                }
            });
        }
    };
});

Thank you!



Sander Elias

unread,
Jan 12, 2017, 2:24:19 AM1/12/17
to Angular
Hi Esa,

Usually, messing with the focus order in this way is frowned upon. It's better to let the browser handle that. There is a whole slew of usability issues concerned with this.

Have a look at the solution I created here for you. It's entirely in ES6, if you need to support old browsers, you can compile it down using babel or typescript.

Regards
Sander

Esa Yletyinen

unread,
Jan 12, 2017, 5:19:12 AM1/12/17
to Angular
Thank you.
 
However, I need this in ES5, preferably in the following form:

app.directive('focusNext', function() {
    return {
        restrict: 'A',
        link: function(scope, el) {
            el.bind('keydown', function(event) {
                if (event.keyCode === 13) {            
        
                    // 1. look for the next element here 
                    // 2. set focus to element

                }
            });
        }
    };
});


I compiled your code with Babel, but the TraverseAndFindFocusableElements it yields is a bit confusing, and doesn't work (regeneratorRuntime is not defined). Can you show me a simple way to find the index of the directives' element inside the among the other items with the same focus-next directive?

I'm able to get the other items document, but none of the items seem to match "el".

var els = document.querySelectorAll('[focus-next]')

for(var i in els) {
console.log(els[i]==el) // all of these return false
}

As you can probably tell, I'm very much a noob with Angular.
 
Cheers.
Esa 

Sander Elias

unread,
Jan 12, 2017, 5:50:50 AM1/12/17
to Angular
What browsers do you need to support? Changes are that generators are in there already, so you don't need the regenarator runtime.

Sander Elias

unread,
Jan 12, 2017, 6:10:24 AM1/12/17
to Angular
Pushed an update with an ES5 traversal.

Here is the result from babel:
'use strict';

function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } }

//es5 helper
function Es5TraverseAndFindFocusableElements(elm) {
   
var result = [];
    inspect
(elm);
   
return result;

   
function inspect(elm) {
       
if (elm.tabIndex > -1) {
            result
.push(elm);
       
}

       
var _arr = [].concat(_toConsumableArray(elm.children));
       
for (var _i = 0; _i < _arr.length; _i++) {
           
var child = _arr[_i];
            inspect
(child);
       
}
   
}
}


function focusNext() {
   
return {
        restrict
: "A", //make it an attribute selector
        controller
: focusNextController
   
};


   
/* ngInject() */
   
function focusNextController($element) {
       
var el = $element[0]; //grab the raw dom element!
       
this.$onInit = function () {
            el
.addEventListener('keydown', handleEnter);
       
};


       
this.$onDestroy = function () {
            el
.removeEventListener('keydown', handleEnter);
       
};

       
function handleEnter(ev) {
           
if (ev.keyCode === 13) {
                ev
.preventDefault();
               
// utilize the above generator and ES6 spread to build an array of focusable elements
               
// now using an ES5 helper.
               
var elmList = Es5TraverseAndFindFocusableElements(document.body);
               
var current = elmList.findIndex(function (n) {
                   
return n == el;
               
});
               
var next = Math.min(elmList.length - 1, current + 1);
               
if (ev.shiftKey) {
                   
//reverse on shift, make it on par with tab.
                   
next = Math.max(0, current - 1);
               
}
                elmList
[next].focus();
           
}
       
}
   
}
}


angular
 
.module('nextEnter', [])
 
.directive('focusNext', focusNext);


Hope this helps you.

Esa Yletyinen

unread,
Jan 13, 2017, 2:49:00 AM1/13/17
to Angular
Thank you very much. My mistake in my previous question was attempting to access the element by "el" rather than "el[0]". I managed to get my version to work.

However, in my case I really do need to access inputs by attribute value, rather than relying on simply the next element in document. I have asked another, a more specific question related to it here: https://groups.google.com/forum/#!msg/angular/k903zBkfZiQ/ai9wxQdNCAAJ;context-place=searchin/angular/dynamically$20%7Csort:relevance

Sander Elias

unread,
Jan 13, 2017, 3:02:01 AM1/13/17
to Angular
Hi Esa,

It doesn't set the focus on the next random element. It sets the focus on the next element THAT CAN RECEIVE focus. It ignores tab-order, but that can be fixed in the code.
If that's not what you need, the code is also easy to adapt to select whatever criteria you might have. Just adapt the if statement in the traverse function. 

Regards
Sander

Esa Yletyinen

unread,
Jan 13, 2017, 4:18:04 AM1/13/17
to Angular
I understand that, but I want it to specifically IGNORE the next element that can receive focus in a specific instance, and set focus on the next element with a specific value of a given attribute, that element being generated by ng-repeat.

As described in the other topic I created, I don't know how to select dynamically generated elements by their attribute, so I don't know how to edit your code appropriately.

Specifically, how do I select e.g. "[my-directive=3]" among these, when all of them has "i" rather than 1, 2, or 3 as the value of [my-directive]: <input ng-repeat="i in [1,2,3]" my-directive="i">

In a specific instance, I want to the focus to move from 1 to 3, rather than the next element. 

Sander Elias

unread,
Jan 13, 2017, 4:41:15 AM1/13/17
to Angular
Hi Esa,

This is where you need to utilize the tabindex attribute (I can adapt my code for that, but it's a bit too much work do do that for just a quick sample...)
You can set the tabindex using your directive. As I don't know what your specific instance is, I can't help you on this. 
Of course, you can ignore existing specs, and write a custom solution for this but I would strongly advise against this.

 <input ng-repeat="i in [1,2,3]" my-directive="i" ng-attr-tabindex="{{someController.calculateOrder(i)}}">

That would make your code follow current specs, and makes it possible to work with both the tab key (standard behavior!) and the directive I have written (once it understands tabIndex.

Regards
Sander

Esa Yletyinen

unread,
Jan 13, 2017, 5:21:43 AM1/13/17
to Angular
Hi,

Here's my situation:

<input type="text" my-directive ng-attr-tabindex="0> // clicking "next" (e.g. tab, enter, down arrow) moves to the first radio input (tabIndex 1)
<input type="radio" my-directive ng-attr-tabindex="1">  // clicking "next" (e.g. tab, enter, down arrow) moves to the last text input (tabIndex 2)
<input type="radio" my-directive ng-attr-tabindex="1">  // clicking "next" (e.g. tab, enter, down arrow) moves to the last text input (tabIndex 2)
<input type="radio" my-directive ng-attr-tabindex="1">  // clicking "next" (e.g. tab, enter, down arrow) moves to the last text input (tabIndex 2)
<input type="text" my-directive ng-attr-tabindex="2">  // clicking "next" (e.g. tab, enter, down arrow) moves to the first text input (tabIndex 0)

All of these elements are generated by ng-repeat.

The enternextfocus directive I showed I my initial question, which accesses the raw DOM element, is used everywhere in the project I'm working on by another programmer, and I cannot start changing that, even if it goes against specs. This is why I was inclined to do it in an un-angular way, and access the element directly. His solution just doesn't work with elements generated by ng-repeat.

Sander Elias

unread,
Jan 13, 2017, 7:23:45 AM1/13/17
to Angular
Aside from the not working with ngRepeat, does the original directive do what it's supposed to do?
Reply all
Reply to author
Forward
0 new messages