How do I get the key up/down directive working on a list in Angularjs?

Problem

I have the following code:

app.directive('ngUp', function () {
    return function (scope, element, attrs) {
        element.bind("keyup", function (event) {
            if(event.which === 40) {
                  console.log('down')
                }
            else if (event.which === 38) {
                  console.log('up');
                }
            else {
                console.log('some other');
            }

        });
    };
});

And I have:

<div  class="scrollbar" id="ex3">
<div>
    <ul>
        <li ng-repeat="video in videos">
            <a href="#" ng-click="select($index)" style="font-size: 100%">
                {{video.name | subStringFilter : 20}}
            </a>
        </li>
    </ul>
</div>

if I put ng-up on li element element or the anchor element I get my element in the directive as a.ng-binding.

I have no idea what this is.

What I want is to be able to scroll up and down my list using my up and down arrow keys (and apply a class to the highlighted list element as I scroll).

I was expecting the element to be either a li element or an anchor element but I get a.ng-binding which does no fire on up or down key presses.

Problem courtesy of: JD.

Solution

Be aware that you need to set tabindex property on the <li> to make it able to trigger keyup event:

<li tabindex="{{$index}}">

Try this directive:

app.directive('ngUp', function() {
  return {
    scope: {
      select: "&"
    },
    link: function(scope, element, attrs) {
      element.on("keyup", "[selectable]", function(event) {
        var $this = $(this);
        var selectedElement = {};

        scope.$apply(function() {
          if (event.which === 40) {
            selectedElement = $this.next("[selectable]");
            if (selectedElement.length > 0) {
              scope.select({
                element: selectedElement
              });
            }
          } else if (event.which === 38) {
            selectedElement = $this.prev("[selectable]");
            if (selectedElement.length > 0) {
              scope.select({
                element: $this.prev("[selectable]")
              });
            }
          } else {

          }
        });

        if (selectedElement.length > 0) {
          $this.blur();
          selectedElement.focus();
        }

      });
    }
  }
});

Use it in the html:

<ul ng-up select="select(element)">
      <li selectable ng-repeat="video in videos" tabindex="{{$index}}" ng-click="select($event.target)" ng-class="{selected:video.selected}">
        <a href="#" style="font-size: 100%">
                {{video.name }}
            </a>
      </li>
  </ul>

DEMO (click to select an element, from then you can use up and down arrow to select)

Explanation:

In my opinion, this directive should be specified on the list items container element (in this case is <ul>), and any selectable items must be applied an attribute selectable. In order to make the directive reusable, this directive only takes care of handling keyup event and select the element, the implementation detail of how the element is selected should be passed as a function:

scope: {
    select: "&"
}

Whenever I need to select an element, I call this bound function:

scope.select({
                    element: $this.prev("[selectable]")
          });

In this example, I assume that the logic to set an element as selected is as follows:

$scope.select = function(element) {
    angular.forEach($scope.videos, function(value) {
      value.selected = false;
    });

    angular.element(element).scope().video.selected = true;
};
Solution courtesy of: Khanh TO

Discussion

There is currently no discussion for this recipe.

This recipe can be found in it's original form on Stack Over Flow.