javascript – What is AngularJS way to create global keyboard shortcuts?-ThrowExceptions

Exception or error:

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.

How to solve:

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

###

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>'
    };
  }
]);

###

Use $document.bind:

function FooCtrl($scope, $document) {
    ...
    $document.bind("keypress", function(event) {
        console.debug(event)
    });
    ...
}

###

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);

###

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.

###

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

###

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.

###

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.

###

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.

###

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>

###

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

###

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>

Leave a Reply

Your email address will not be published. Required fields are marked *