Change row selection using arrows in ng-repeat

March 15, 2015
by Rahil Shaikh

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
<style>
.selected {
    background-color:black;
    color:white;
    font-weight:bold;
}
</style>
<html>
    <head>
        <link href="css/bootstrap.min.css" rel="stylesheet" media="screen" />
        <link href="css/bootstrap-theme.min.css" rel="stylesheet" media="screen"/>
    </head>
    <body ng-app="foodApp" ng-controller="foodCtrl">
        <table class="table table-bordered">
            <tr>
                <th>#</th>
                <th>Name</th>
                <th>Price</th>
                <th>Quantity</th>
            </tr>
            <tr ng-repeat="item in foodItems" ng-class="{'selected':$index == selectedRow}" ng-click="setClickedRow($index)">
                <td>{{$index}}</td>
                <td>{{item.name}}</td>
                <td>{{item.price}}</td>
                <td>{{item.quantity}}</td>
            </tr>
        </table>
        <div>
            selectedRow = {{selectedRow}}
        </div>
    <script src="js/angular.js"></script>
    <script src="js/foodCtrl.js"></script>
    </body>
</html>

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;<br />
            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;<br />
            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
<table class="table table-bordered" arrow-selector>
....
</table>

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:

 <tr id="row_{{$index}}" ng-repeat="item in foodItems" ng-class="{'selected':$index == selectedRow}"                     ng-click="setClickedRow($index)"> </tr>

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.

About

Engineer. Blogger. Thinker. Loves programming and working with emerging tech. We can also talk on Football, Gaming, World Politics, Monetary Systems.

Get notified on our new articles

Subscribe to get the latest on Node.js, Angular, Blockchain and more. We never spam!
First Name:
Email:

15 comments

  1. harini
    |

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

    The selected row is not getting highlighted using Mouse after using Arrow for selection

  4. Samad
    |

    Hi,
    Thanks for this post. Is there a way to have a “ControllerAS” version of this code?

  5. Samad
    |

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

  6. Adi
    |

    Great post!

  7. AHAMED NAZUMUDEEN
    |

    How can i select multiple rows?
    could you give me a sample

  8. sada sa
    |

    Hi i need to highlight a particular row using previous and next button. Any idea how to do?

    • |

      You would only need to replace the keycodes to the ones you want to use.

  9. juan
    |

    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?

    • |

      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.

  10. |

    HI Rahil,

    Rather than using “scope.foodItems.length” is there any other way we can send the data set to the directive

    • |

      Is this possible? I am trying to make a generic solution but it seems that the directive is bound to the scope.

  11. |

    Excellent article. Thank you!

  12. |

    Great Work but sir also explain how to move the cursor with the arrow up and arrow down to a record to focus the record and when the user press enter on the selected record an action is performed

Leave a Comment