In this post we will implement row selection in ng-repeat using arrow keys.Earlier we had implement row selection in ng-repeat where when the user clicks on a row it gets highlighted.
In this post we will go one step ahead and allow the user to change row selection using arrow keys.
In case you are looking for standard Highlighting a row in ng-repeat check this post.

Highlight a selected row in ng-repeat using ng-class

Objective

Here we will be building a custom directive that will enable row selection using arrow keys.

DEMO  DOWNLOAD

What we Already Have

I’ll be building this, on, one of my earlier post, that was Highlighting row in ng-repeat.Which allows user to select rows on click. Although not necessary but arrow navigation is an enhancement where as selection on click is desired in most cases.

foodCtrl.js


var foodApp = angular.module('foodApp',[]);


foodApp.controller('foodCtrl',function($scope){
$scope.selectedRow = 0;
$scope.foodItems = [{
name:'Noodles',
price:'10',
quantity:'1'
},
{
name:'Pasta',
price:'20',
quantity:'2'
},
{
name:'Pizza',
price:'30',
quantity:'1'
},
{
name:'Chicken tikka',
price:'100',
quantity:'1'
}];
$scope.setClickedRow = function(index){
$scope.selectedRow = index;
}
});

index.html

# Name Price Quantity
{{$index}} {{item.name}} {{item.price}} {{item.quantity}}

selectedRow = {{selectedRow}}



Here Above in foodCtrl we have:
$scope.selectedRow which holds the value of current selected row.
$scope.setClickedRow is a function that sets $scope.selectedRow to the index of the row clicked.
$scope.foodItems is an array which holds the list of food items on which we iterate using ng-repeat.

In our html file we have defined a class as .selected and this class is applied to the row when the user clicks on it,this done using ng-class directive.

Building our arrow selector directive
foodCtrl.js


foodApp.directive('arrowSelector',['$document',function($document){
return{
restrict:'A',
link:function(scope,elem,attrs,ctrl){
var elemFocus = false;
elem.on('mouseenter',function(){
elemFocus = true;
});
elem.on('mouseleave',function(){
elemFocus = false;
});
$document.bind('keydown',function(e){
if(elemFocus){
if(e.keyCode == 38){
console.log(scope.selectedRow);
if(scope.selectedRow == 0){
return;
}
scope.selectedRow--;
scope.$apply();
e.preventDefault();
}
if(e.keyCode == 40){
if(scope.selectedRow == scope.foodItems.length - 1){
return;
}
scope.selectedRow++;
scope.$apply();
e.preventDefault();
}
}
});
}
};
}]);

Video

Directive explained

Here above I’ve created an arrowSelector directive for our application, $document service is injected as dependency , that is equivalent to window.document of plain javascript.
restrict:’A’ notifies angular js that our directive is an attribute directive.
link: This is where usually dom manipulations are kept in angular js.

foodCtrl.js


var elemFocus = false;
elem.on('mouseenter',function(){
elemFocus = true;
});
elem.on('mouseleave',function(){
elemFocus = false;
});

elem is the html element on which our directive is declared.
We have defined a variable elemFocus which is by default false and changes to true when mouse pointer enters our element and again changes to false when the mouse pointer leaves our element.

This Boolean variable will help us to use arrow keys only when the pointer is on the table(which is our element here).

foodCtrl.js


$document.bind('keydown',function(e){
...
....
});

Here keydown event is bound to the $document. The reason I’ve not bound this on the element it self ,is that for the element to listen to the event it has to be in focused state.So in that case the user would have had to first click on the element(table in our case) once, to use the arrow keys.
This is also the reason we are setting the value of elemFocus variable on mouse events, so that we are able to decide when the mouse-pointer is on the element.

foodCtrl.js


if(elemFocus){
if(e.keyCode == 38){
if(scope.selectedRow == 0){
return;
}
scope.selectedRow--;
scope.$apply();
e.preventDefault();
}
if(e.keyCode == 40){
if(scope.selectedRow == scope.foodItems.length - 1){
return;
}
scope.selectedRow++;
scope.$apply();
e.preventDefault();
}
}

Here as you can see we are first checking if the mouse is on the element.Then we are checking for the keyCode on the event . Keycode 38 is for Up-arrow and 40 is for Down-arrow
On hit of Up key we first check if the selectedRow is 0,if it is not 0 we proceed and decrement it by one.
Next we use scope.$apply(), this is to integrate our changes into the $digest cycle and keep our js and UI in sync.
Lastly we restrict the default functionality of the key by using e.preventDefault()

On press of down key we are incrementing scope.selectedRow by one, but if the value of scope.selectedRow is equal to the length of array minus 1 , we do nothing because we are already on the last row.

Using our directive in html

index.html

....

Above we have added our arrow-selector directive on the table.

Finally $watch the variable:
foodCtrl.js


$scope.$watch('selectedRow', function() {
console.log('Do Some processing'); //runs the block whenever selectedRow is changed.
});

Add this to your controller if you want to run some code when ever the row selection is changed.

Alternate Method

If we want to keep your processing in $scope.setClickedRow() instead of using $watch, we will have to add a unique id to each table row .
example:

So your 1st row will have id = “row_0” ;2nd will have id =”row_1″ and so on.

And in our directive inside the keycode check block we will programmatically trigger click using

angular.element(‘#row_’+(scope.selecedRow+1)).click();

scope.selecedRow plus one or minus one depending on which key is hit and this in turn will call $scope.setClickedRow() function and hence change the row selection.

Conclusion

Although not necessary but row selection using arrows will add a cool feature to your application and enhance user experience.
Feel free to drop your reviews in the comments below.

11 Comments

  1. Great post!

    But can you please tell me how to select a field alone ( like name here) rather than selecting whole row? Please reply

  2. I replied on your facebook post !
    Based on the plnkr you provided this should do it.

    ng-class=”{‘selected’:item.inputDirty}”

    $scope.setClickedRow = function(index){
    var val= $scope.user.name[index];
    $scope.row = index;
    if(angular.isUndefined(val) || val === null){
    $scope.foodItems[index].inputDirty = true; }
    else{
    $scope.foodItems[index].inputDirty = false; }
    }

  3. I`m trying to make it work as a component instead of directive by using ng-keydown? what do you think

  4. Hi tks , i need, when there are a lot of rows, i move using arrow keys but the scroll don`t move. Any idea how to do?

    1. Hi Juan, that’s a good use case to work on. Will do once I get some spare time. Mean while you are invited as well if you can figure out a way to make it work. Here is the repo, you may fork it and send a pull request, I’ll check it and merge.

Leave a Reply

Your email address will not be published.