77

I suppose that I should use directive, but it seems strange to add directive to body, but listen events on document.

What is a proper way to do this?

UPDATE: Found AngularJS UI and saw their realization of keypress directive.

Mark Chackerian
  • 21,866
  • 6
  • 108
  • 99
ValeriiVasin
  • 8,628
  • 11
  • 58
  • 78
  • 1
    I assume you mean keyboard shortcuts... I've been curious about this too, I'm coming to the conclusion that angular isn't the best tool for this task. I wrote a directive that does this but there are problems - first is the semantic one you are alluding too, also I don't think it's considered a good practice to wrap jquery in a directive, and it has led to some confusing situations when there are multiple templates only some of which needs the document shortcuts. – Jason Feb 23 '13 at 19:28
  • Shortcuts need to be connected with my controller. And I don't see any benefits of external jquery module. Also two possible ways I see: 1) jQuery external shortcuts module + pubsub communication with controller. 2) Angular directive, which is strange, but I suppose it's ok to provide link function with shortcuts. – ValeriiVasin Feb 23 '13 at 19:31
  • I don't think you could add the angularjs ui directives to the document, they are scoped to an element. – Jason Feb 23 '13 at 20:04
  • Yep, but I see that proper way is directive, as I supposed. Also I'm not going add UI to my project, will implement something like you proposed. – ValeriiVasin Feb 23 '13 at 20:15
  • 3
    don't need an extra library... use `$document.bind('keypress')` See [$document](http://docs.angularjs.org/api/ng.$document) – charlietfl Feb 23 '13 at 20:42
  • Thanks, of course I will use this pattern :) – ValeriiVasin Feb 23 '13 at 20:49
  • 2
    The link is now a 404. If there's an updated location, please can you update it. – JsAndDotNet Aug 06 '15 at 13:08

12 Answers12

70

I would say a more proper way (or "Angular way") would be to add it to a directive. Here's a simple one to get you going (just add keypress-events attribute to <body>):

angular.module('myDirectives', []).directive('keypressEvents', [
  '$document',
  '$rootScope',
  function($document, $rootScope) {
    return {
      restrict: 'A',
      link: function() {
        $document.bind('keypress', function(e) {
          console.log('Got keypress:', e.which);
          $rootScope.$broadcast('keypress', e);
          $rootScope.$broadcast('keypress:' + e.which, e);
        });
      }
    };
  }
]);

In your directive you can then simply do something like this:

module.directive('myDirective', [
  function() {
    return {
      restrict: 'E',
      link: function(scope, el, attrs) {
        scope.keyPressed = 'no press :(';
        // For listening to a keypress event with a specific code
        scope.$on('keypress:13', function(onEvent, keypressEvent) {
          scope.keyPressed = 'Enter';
        });
        // For listening to all keypress events
        scope.$on('keypress', function(onEvent, keypressEvent) {
          if (keypress.which === 120) {
            scope.keyPressed = 'x';
          }
          else {
            scope.keyPressed = 'Keycode: ' + keypressEvent.which;
          }
        });
      },
      template: '<h1>{{keyPressed}}</h1>'
    };
  }
]);
jmagnusson
  • 5,799
  • 4
  • 43
  • 38
  • 2
    Way to go, really clean. – DevLounge Oct 23 '13 at 06:47
  • 2
    It was binding to the $document, not the element, that worked for me for getting key events on a div. +1 for showing how to inject $document as well. – prototype Nov 12 '13 at 20:05
  • 6
    According to code above this directive binds event to window.document ($document) element while it can be attached to any DOM tag, not only , as there is no validation. In this case element to which directive is attached can be destroyed, but binded event listener will remain. I would recomend to either put there some validation (to restrict element to ) or implement method that will unbind event listener using $scope.on('destroy'). – chmurson Sep 24 '14 at 08:21
  • 1
    Why it doesn't capture `Escape` key? – Saeed Neamati Feb 21 '15 at 08:12
  • @SaeedNeamati See Yehuda Katz's reply to Resig on the subject http://ejohn.org/blog/keypress-in-safari-31/ – jmagnusson Feb 26 '15 at 10:54
  • Angular comes with built-in key listener directives like ngKeypress and ngKeydown... Why not just use these? See http://stackoverflow.com/a/22246081/1736012 – olefrank Feb 28 '15 at 20:46
  • @olefrank this solution was created as a reusable directive. I guess you could use your suggestion if you don't need that. – jmagnusson Mar 03 '15 at 14:29
27

Use $document.bind:

function FooCtrl($scope, $document) {
    ...
    $document.bind("keypress", function(event) {
        console.debug(event)
    });
    ...
}
erbridge
  • 1,376
  • 12
  • 27
user1338062
  • 11,939
  • 3
  • 73
  • 67
  • It seems there are two approaches with this, one is to create a directive and pass the `$event` into the controller via a function, the other is to bind the event directly in the controller. The controller method seems less code and the same result. Is there a reason to choose one method over the other? – Owen Allen Jan 16 '15 at 05:10
  • this approach was giving me multiple event triggers with each keypress, I used mousetrap instead of $document.bind and it seems to suffice quite well. – rwheadon Feb 23 '15 at 02:34
  • this approach disabled the automatic update for other variables, {{ abc }} – windmaomao Mar 28 '15 at 18:02
  • 1
    You need to apply the change `var that = this; $document.bind("keydown", function(event) { $scope.$apply(function(){ that.handleKeyDown(event); });` – FreshPow May 31 '17 at 19:47
20

I can't vouch for it just yet but I've started taking a look at AngularHotkeys.js:

http://chieffancypants.github.io/angular-hotkeys/

Will update with more info once I've got my teeth into it.

Update 1: Oh there's a nuget package: angular-hotkeys

Update 2: actually very easy to use, just set up your binding either in your route or as I'm doing, in your controller:

hotkeys.add('n', 'Create a new Category', $scope.showCreateView);
hotkeys.add('e', 'Edit the selected Category', $scope.showEditView);
hotkeys.add('d', 'Delete the selected Category', $scope.remove);
A. Murray
  • 2,761
  • 5
  • 27
  • 40
10

Here's how I've done this with jQuery - I think there's a better way.

var app = angular.module('angularjs-starter', []);

app.directive('shortcut', function() {
  return {
    restrict: 'E',
    replace: true,
    scope: true,
    link:    function postLink(scope, iElement, iAttrs){
      jQuery(document).on('keypress', function(e){
         scope.$apply(scope.keyPressed(e));
       });
    }
  };
});

app.controller('MainCtrl', function($scope) {
  $scope.name = 'World';
  $scope.keyCode = "";
  $scope.keyPressed = function(e) {
    $scope.keyCode = e.which;
  };
});
<body ng-controller="MainCtrl">
  <shortcut></shortcut>
  <h1>View keys pressed</h1>
  {{keyCode}}
</body>

Plunker demo

JJJ
  • 32,902
  • 20
  • 89
  • 102
Jason
  • 15,915
  • 3
  • 48
  • 72
  • Thanks for response. I see that you think we should do it as directive. – ValeriiVasin Feb 23 '13 at 20:00
  • link: function postLink(scope, iElement, iAttrs){ window.addEventListener('load', function(e){ scope.$apply(scope.keyPressed(e)); }, false); } – devnill Mar 28 '14 at 19:02
  • strange to have a directive for this, but not service. directive - a reusable ui component (in most cases). – ses Apr 23 '15 at 13:14
  • Every time I hear about JQuery inside Angular I get goose bumps – walox Feb 21 '17 at 16:42
10

Here's an example of an AngularJS service for keyboard shortcuts: http://jsfiddle.net/firehist/nzUBg/

It can then be used like this:

function MyController($scope, $timeout, keyboardManager) {
    // Bind ctrl+shift+d
    keyboardManager.bind('ctrl+shift+d', function() {
        console.log('Callback ctrl+shift+d');
    });
}

Update: I'm now using angular-hotkeys instead.

Tom Söderlund
  • 4,743
  • 4
  • 45
  • 67
7

As a Directive

This is essentially how it is done in the Angular documentation code, i.e. pressing / to start searching.

angular
 .module("app", [])
 .directive("keyboard", keyboard);

function keyboard($document) {

  return {
    link: function(scope, element, attrs) {

      $document.on("keydown", function(event) {

      // if keycode...
      event.stopPropagation();
      event.preventDefault();

      scope.$apply(function() {            
        // update scope...          
      });
    }
  };
}

Plunk using a keyboard directive

http://plnkr.co/edit/C61Gnn?p=preview


As a Service

Converting that directive into a service is real easy. The only real difference is that the scope is not exposed on the service. To trigger a digest, you can bring in the $rootScope or use a $timeout.

function Keyboard($document, $timeout, keyCodes) {
  var _this = this;
  this.keyHandlers = {};

  $document.on("keydown", function(event) {        
    var keyDown = _this.keyHandlers[event.keyCode];        
    if (keyDown) {
      event.preventDefault();
      $timeout(function() { 
        keyDown.callback(); 
      });          
    }
  });

  this.on = function(keyName, callback) {
    var keyCode = keyCodes[keyName];
    this.keyHandlers[keyCode] = { callback: callback };
    return this;
  };
}

You can now register callbacks in your controller using the keyboard.on() method.

function MainController(keyboard) {

  keyboard
    .on("ENTER",  function() { // do something... })
    .on("DELETE", function() { // do something... })
    .on("SHIFT",  function() { // do something... })
    .on("INSERT", function() { // do something... });       
}

Alternate version of Plunk using a service

http://plnkr.co/edit/z9edu5?p=preview

Blake Bowen
  • 1,049
  • 1
  • 12
  • 36
4

The slightly shorter answer is just look at solution 3 below. If you would like to know more options, you could read the whole thing.

I agree with jmagnusson. But I believe there is cleaner solution. Instead of binding the keys with functions in directive, you should be able just bind them in html like defining a config file, and the hot keys should be contextual.

  1. Below is a version that use mouse trap with a custom directive. (I wasn't the author of this fiddle.)

    var app = angular.module('keyExample', []);
    
    app.directive('keybinding', function () {
        return {
            restrict: 'E',
            scope: {
                invoke: '&'
            },
            link: function (scope, el, attr) {
                Mousetrap.bind(attr.on, scope.invoke);
            }
        };
    });
    
    app.controller('RootController', function ($scope) {
        $scope.gotoInbox = function () {
            alert('Goto Inbox');
        };
    });
    
    app.controller('ChildController', function ($scope) {
        $scope.gotoLabel = function (label) {
            alert('Goto Label: ' + label);
        };
    });
    

    You will need to include mousetrap.js, and you use it like below:

    <div ng-app="keyExample">
        <div ng-controller="RootController">
            <keybinding on="g i" invoke="gotoInbox()" />
            <div ng-controller="ChildController">
                <keybinding on="g l" invoke="gotoLabel('Sent')" />
            </div>
        </div>
        <div>Click in here to gain focus and then try the following key strokes</div>
        <ul>
            <li>"g i" to show a "Goto Inbox" alert</li>
            <li>"g l" to show a "Goto Label" alert</li>
        </ul>
    </div>
    

    http://jsfiddle.net/BM2gG/3/

    The solution require you to include mousetrap.js which is library that help you to define hotkeys.

  2. If you want to avoid the trouble to develop your own custom directive, you can check out this lib:

    https://github.com/drahak/angular-hotkeys

    And this

    https://github.com/chieffancypants/angular-hotkeys

    The second one provide a bit more features and flexibility, i.e. automatic generated hot key cheat sheet for your app.

Update: solution 3 is no longer available from Angular ui.

  1. Apart from the solutions above, there is another implementation done by angularui team. But the downside is the solution depends on JQuery lib which is not the trend in the angular community. (Angular community try to just use the jqLite that comes with angularjs and get away from overkilled dependencies.) Here is the link

    http://angular-ui.github.io/ui-utils/#/keypress

The usage is like this:

In your html, use the ui-keydown attribute to bind key and functions.

<div class="modal-inner" ui-keydown="{
                        esc: 'cancelModal()',
                        tab: 'tabWatch($event)',
                        enter: 'initOrSetModel()'
                    }">

In your directive, add those functions in your scope.

app.directive('yourDirective', function () {
   return {
     restrict: 'E',
     templateUrl: 'your-html-template-address.html'
     link: function(){
        scope.cancelModal() = function (){
           console.log('cancel modal');
        }; 
        scope.tabWatch() = function (){
           console.log('tabWatch');
        };
        scope.initOrSetModel() = function (){
           console.log('init or set model');
        };
     }
   };
});

After playing around with all of the solutions, I would recommend the one that is implemented by Angular UI team, solution 3 which avoided many small strange issues I have encountered.

Tim Hong
  • 2,734
  • 20
  • 23
  • 1
    Thanks for sharing. I have up voted it. Actually chieffancypants' angular hotkey looks amazing but don't know how to customize it only to a particular model. drahak's angular-hotkeys works good for one to one relationships! – Learner_Programmer Nov 13 '14 at 09:57
  • I guess this third solution (ui-utils) is no longer maintained or the link is invalid. The github repo is marked as deprecated – Bernardo Ramos May 17 '16 at 09:38
  • Thanks for the comment Bernardo, I will remove the third solution. – Tim Hong May 17 '16 at 11:51
1

I made a service for shortcuts.

It looks like:

angular.module('myApp.services.shortcuts', [])
  .factory('Shortcuts', function($rootScope) {
     var service = {};
     service.trigger = function(keycode, items, element) {
       // write the shortcuts logic here...
     }

     return service;
})

And I injected it into a controller:

angular.module('myApp.controllers.mainCtrl', [])
  .controller('mainCtrl', function($scope, $element, $document, Shortcuts) {
   // whatever blah blah

   $document.on('keydown', function(){
     // skip if it focused in input tag  
     if(event.target.tagName !== "INPUT") {
        Shortcuts.trigger(event.which, $scope.items, $element);
     }
   })
})

It works, but you may notice that I inject $element and $document into the controller.

It's a bad controller practice and violates the 'Dont EVER access $element in the controller' convention.

I should put it into directive, then use 'ngKeydown' and $event to trigger the service.

But I think the service is fine and I will rework the controller sooner.


updated:

It seems like 'ng-keydown' only works in input tags.

So I just write a directive and inject $document:

angular.module('myApp.controllers.mainCtrl', [])
  .directive('keyboard', function($scope, $document, Shortcuts) {
   // whatever blah blah
   return {
     link: function(scope, element, attrs) {
       scope.items = ....;// something not important

       $document.on('keydown', function(){
         // skip if it focused in input tag  
         if(event.target.tagName !== "INPUT") {
           Shortcuts.trigger(event.which, scope.items, element);
         }
       })
     }
   }
  })

It's better.

zisasign
  • 83
  • 1
  • 6
0

Check this example from the guys behid ng-newsletter.com; check their tutorial on creating a 2048 game, it has some nice code using a service for keyboard events.

numediaweb
  • 16,362
  • 12
  • 74
  • 110
0

The following let's you write all your shortcut logic in your controller and the directive will take care of everything else.

Directive

.directive('shortcuts', ['$document', '$rootScope', function($document, $rootScope) {
    $rootScope.shortcuts = [];

    $document.on('keydown', function(e) {
        // Skip if it focused in input tag.
        if (event.target.tagName !== "INPUT") {
            $rootScope.shortcuts.forEach(function(eventHandler) {
                // Skip if it focused in input tag.
                if (event.target.tagName !== 'INPUT' && eventHandler)
                    eventHandler(e.originalEvent, e)
            });
        }
    })

    return {
        restrict: 'A',
        scope: {
            'shortcuts': '&'
        },
        link: function(scope, element, attrs) {
            $rootScope.shortcuts.push(scope.shortcuts());
        }
    };
}])

Controller

    $scope.keyUp = function(key) {
        // H.
        if (72 == key.keyCode)
            $scope.toggleHelp();
    };

Html

<div shortcuts="keyUp">
    <!-- Stuff -->
</div>
Reimund
  • 2,346
  • 1
  • 21
  • 25
0

you can try this library it made it very easy to manage hot keys, it automatically binds and unbinds keys as you navigate the app

angular-hotkeys

Ahmed Ahmed
  • 1,036
  • 11
  • 16
0

i don't know if it's a real angular way, but what i've done

$(document).on('keydown', function(e) {
    $('.button[data-key=' + String.fromCharCode(e.which) + ']').click();
});

<div class="button" data-key="1" ng-click="clickHandler($event)">
    ButtonLabel         
</div>
wutzebaer
  • 14,365
  • 19
  • 99
  • 170