Catch all key press events

2,966 views
Skip to first unread message

Arni Arent

unread,
Oct 15, 2013, 8:45:54 PM10/15/13
to ang...@googlegroups.com
I want to make a specialized singlepage app with AngularJS that does not allow mouse interaction, but only keyboard interaction.

What I want to do is capture all keypress events and control the state of the app according to what keys are pressed.

My guess is I need to first capture the keypress event and then send it to a controller that needs to act accordingly.

So, any thoughts?

How would I capture all the keypress events in AngularJS (I know I can capture with window.onkeypress)
How would I send them to the active controller?

Howard.zuo

unread,
Oct 16, 2013, 4:29:31 AM10/16/13
to ang...@googlegroups.com
You'd better using directive for this purpose, see below example:
 
var testModule = angular.module('TestModule', []);

testModule.directive('keyCapture', [function() {
  return {
    link: function (scope, element, attrs, controller) {
         console.log(element);
              element.on('keydown', function(e){
               console.log(e.keyCode);
              });
      
      }
    }
  }]
);


<!doctype html>
<html lang="en" ng-app="TestModule">
<head>
<meta charset="UTF-8">
<title>Test capture key event</title>
</head>
<body key-capture>

</body>
</html>
<script type="text/javascript" src="libs/JQuery-2.0.3.js"></script>
<script type="text/javascript" src="libs/angular-1.0.8.js"></script>
<script type="text/javascript" src="apps/TestModule.js"></script>
 

Arni Arent

unread,
Oct 16, 2013, 5:08:37 PM10/16/13
to ang...@googlegroups.com
So, what would be the best way to get the event to the active controller? Here's my test module:

var app = angular.module('app', []);
app.config(function($routeProvider) {
    $routeProvider.when("/home", {
        templateUrl: "partials/home.html",
        controller: HomeCtrl
    });
    $routeProvider.otherwise({redirectTo: "/home"});
});

app.directive('keyCapture', [function() {

    return {
        link: function (scope, element, attrs, controller) {
            element.on('keydown', function(e){
                console.log(e.keyCode);
                console.log(app);
                app.controller.eventInput(e.keyCode);
            });
        }
    }
}]);

function HomeCtrl($scope, $location) {
    console.log("Home controller initiated");
    this.eventInput = function(code) {
        console.log("controller got code: " + code);
    }
}



<!doctype html>
<html lang="en" ng-app="app">
<head>
<meta charset="UTF-8">
<title>Test</title>
</head>
<body key-capture><ng-view></ng-view></body>
</html>

<script type="text/javascript" src="jquery-2.0.3.min.js"></script>
<script type="text/javascript" src="angular.min.js"></script>
<script type="text/javascript" src="app.js"></script>

Chris Rhoden

unread,
Oct 16, 2013, 6:41:55 PM10/16/13
to ang...@googlegroups.com
app.directive('ngKeyDown', [function() {
    return {
        restrict: 'A',
        scope: { ngKeyDown: '&' },
        link: function (scope, element, attrs) {
            element.on('keydown', function(e){
                scope.$apply(function () {
                    scope.ngKeyDown({$keyCode: e.keyCode, $event: e});
                });
            });
        }
    }
}]);

<div ng-controller="HomeCtrl as ctrl" ng-key-down="ctrl.eventInput($keyCode)"></div>


--
You received this message because you are subscribed to the Google Groups "AngularJS" group.
To unsubscribe from this group and stop receiving emails from it, send an email to angular+u...@googlegroups.com.
To post to this group, send email to ang...@googlegroups.com.
Visit this group at http://groups.google.com/group/angular.
For more options, visit https://groups.google.com/groups/opt_out.



--
chrisrhoden

Arni Arent

unread,
Oct 16, 2013, 6:54:44 PM10/16/13
to ang...@googlegroups.com
Nope, doesn't work.

Error: Argument 'HomeCtrl as ctrl' is not a function, got undefined
    at Error (<anonymous>)


I think angular is maybe not best for this project. I'll do a jquery/requirejs app.

Chris Rhoden

unread,
Oct 16, 2013, 7:00:54 PM10/16/13
to ang...@googlegroups.com
The code I sent was based on your continuing to use your existing controller.

Chris Rhoden

unread,
Oct 16, 2013, 7:08:16 PM10/16/13
to ang...@googlegroups.com
On Wed, Oct 16, 2013 at 6:54 PM, Arni Arent <arni...@gmail.com> wrote:
I think angular is maybe not best for this project. I'll do a jquery/requirejs app.

If the work required to learn what the 4 changes I made to the code you sent do is too much of an investment for the project, you are absolutely correct. jQuery has been around for a while and I suspect that you know it pretty well. If you're not going to build the kind of app that Angular is designed for, then you should not use Angular. Further, it's not going to be something that you pick up after 2 messages to a mailing list and reading a single tutorial. I think if you're honest with yourself, jQuery wasn't, either.

I can tell you that I have built an app that I think works similarly to your description, and found Angular to be a much better way of building that app than just jQuery. I think that by the time you're talking about building an app that is large enough to warrant the use of requirejs for organizational purposes, something beyond jQuery might be worth looking into. It doesn't need to be Angular, though! But if you're actually interested in learning Angular or evaluating it, I would encourage you to give it a little bit more effort.

Best,

--
chrisrhoden

Arni Arent

unread,
Oct 16, 2013, 8:27:10 PM10/16/13
to ang...@googlegroups.com
All this code seems very complex to me, and I've used javascript for a long time. Not that the syntax is unreadable, but the workings of angular are just too complex. I have no idea why you do restrict:'A' or scope: { ngKeyDown: '&' }, I mean, why 'A' and why '&'?

AngularJS is great, but also somewhat strange. It can offer you great simplicity in solving complex things, but doing something simple can be very complex. I've used it and continue to use it for other projects, but I find it difficult to use it for some scenarios.
 
I have implemented your code exactly as intended.





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

app.config(function($routeProvider) {


    $routeProvider.when("/home", {
        templateUrl: "partials/home.html",
        controller: HomeCtrl
    });
   
    $routeProvider.otherwise({redirectTo: "/home"});

});

app.directive('keyCapture', [function() {
    return {
        link: function (scope, element, attrs, controller) {
            element.on('keydown', function(e){
                scope.$emit('TEST');
                console.log(e.keyCode);
            });
        }
    }
}]);


app.directive('ngKeyDown', [function() {
    return {
        restrict: 'A',
        scope: { ngKeyDown: '&' },
        link: function (scope, element, attrs) {
            element.on('keydown', function(e){
                scope.$apply(function () {
                    scope.ngKeyDown({$keyCode: e.keyCode, $event: e});
                });
            });
        }
    }
}]);

function HomeCtrl($scope, $location) {
    console.log("Home controller initiated");

    this.eventInput = function(event) {
        console.log("GOT EVENT: " + event);
    };
}




index.html:

<!doctype html>
<html lang="en" ng-app="app">
<head>
<meta charset="UTF-8">
<title>Test</title>
</head>
<body key-capture><ng-view></ng-view></body>
</html>

<script type="text/javascript" src="jquery-2.0.3.min.js"></script>
<script type="text/javascript" src="angular.min.js"></script>
<script type="text/javascript" src="app.js"></script>



home.html:
Welcome home

<div ng-controller="HomeCtrl as ctrl" ng-key-down="ctrl.eventInput($keyCode)"></div>



Howard.zuo

unread,
Oct 16, 2013, 11:30:22 PM10/16/13
to ang...@googlegroups.com
Hi Arni Arent,
My guess is that you are new to AngularJS, and i totally agree with you, it's hard to get everything started by using AngularJS, since it offers us a lot of new notions.
The basic idea of AngularJs is to make your project as modular as possible.

I am trying to correct a bit the wrong usages in your test module:
1. You broken the DI rules in the directive, controller couldn't called in that way.
2. The question is what stuff you want to placed at controller? According to the documentation of AngularJS, controller should only focus on the logic, and nothing to do with user interaction.

I've uploaded the example code as test.zip, check it out. If any problem, just let me know.

在 2013年10月17日星期四UTC+8上午5时08分37秒,Arni Arent写道:
So, what would be the best way to get the event to the active controller? Here's my test module:

var app = angular.module('app', []);
......
......
......
      
test.zip

Arni Arent

unread,
Oct 17, 2013, 4:40:37 AM10/17/13
to ang...@googlegroups.com
Thanks a lot for the example code, amazing you go into that effort.

Regarding point #2, a controller does handle what occurs after a user action, whether it be a button press or a key press!?
For instance, if a user presses some button a certain things should happen. I modified the controller to detect keyCode changes:

testModule.controller('HomeController', ['$scope', function($scope){
    $scope.$watch('keyCode', function() {
      console.log("controller: " + $scope.keyCode);
    });
}]);

Chris Rhoden

unread,
Oct 17, 2013, 11:32:00 AM10/17/13
to ang...@googlegroups.com
Ok, let's walk through why accessing the DOM from your controllers or controllers from
your directives are bad ideas. In the process, I will also explain what the changes I made are, and how you can
find out what they are for yourself in the future. Finally, I will explain how having a directive directly modify scope
in the way that Howard has recommended, while something that I did a few times when I was first learning Angular,
makes things harder for you in the long run.

Two of they key pillars of Angular app design are testability and separation of concerns. They go hand in hand, 
because in order to have a well tested app, it should be easy to test, and to be easy to test you need to have a
good separation of concerns. For this reason, absolutely all DOM interaction should happen in directives. 
When you're testing a directive, it's usually as easy as something like this:
    
    var element = angular.element('<div my-directive></div>');
    element.keyDown({...});
    expect(element...

As you can see, by keeping all of your DOM interactions limited to happening in directives, you can very easily
test them without ever attaching them to the DOM itself, making it very easy to test in isolation. Striving for testable
code also usually results in directives that do very little (so that they are easy to test in completion), which often
leads to writing reusable, modular code.

However, if you want to write code that is useful outside of your own app, and resilient to being moved from place to
place within your own app, you need to make sure that your directives don't assume anything about the context in
which they will be run. This includes reading from / writing to the scope (unless it is an isolate scope, more on this
later), or accessing the active controller (unless it is a directive controller, which is just a way for directives to depend
upon and communicate with one another). But how do we get information from our DOM interactions to our app code?

You can use events, as you have done here, but that's a pretty blunt tool. Because of the way that scopes work,
it is impossible to prevent an event that is crawling down the scope tree (to the leaves) from propagating further, so
events are really only useful when you want multiple pieces of your application to be able to respond to the event. And
because we're doing our best to avoid assuming the context in which our directives will be used, we can't assume that
is the case, so we turn to another option.

You could write directly to the active scope and rely on the controller to $watch or otherwise react to those changes, but
again, this assumes an awful lot about the directive's context - what if I want to move this to a different controller that
doesn't have the same methods implemented or with a different signature? What if there is a name conflict with an existing
value on the scope? (My experience has dictated that, while often very useful and the only way to do things, $watch is a
code smell and if you find yourself reaching for it you should consider whether there's an easier way.)

Lastly, you can take advantage of isolate scopes and declare your wiring in the view/template. That's what I did in the
example I sent you.

    ....
    restrict: 'A',
    scope: { ngKeyDown: '&' },
    ...

The restrict attribute of a directive declaration refers to how it can be used in views. In this case, A means that it should
be used as an attribute, meaning that it's not a valid use to just have an element with that name, but it can be added to
any existing element.

The scope attribute is a little more confusing, but only just. Basically, I am saying that the ng-key-down attribute on the element
should be treated as a callable expression and passed in on my directive's scope as ngKeyDown. You can learn more about
isolate scopes and their declaration on the directives guide (http://docs.angularjs.org/guide/directive).

The effect of that declaration is that I can write the following markup:

    <div ng-key-down="console.log('key down!')"></div>

...and my directive's link function has been called. It has been passed an isolate scope with a single declaration - ngKeyDown -
which is a function that I can call.

Let's declare my link function like this: 

    function (scope, el) {
        el.on('keydown', function (event) {
            scope.ngKeyDown();
        });
    }

Now, every time the DOM fires the keydown event on the element that this directive is used on, it will call through to the
expression we defined in our view - in this case, logging to the console.

This isn't super useful, though, because while we know that a key was pressed, we don't know which key. If we pass an object
into the function defined on our isolate scope, we can define local variables that will be available to our expression, like so:

    <div ng-key-down="console.log($keyCode)"></div>

    ...
    scope.ngKeyDown({'$keyCode', event.keyCode});
    ...

This might seem like a lot of extra work, but let's talk about what we have gotten for that work. First, you can use this multiple times
on the same page, so that different parts of the app activate different things. Second, you have completely decoupled your app code
from your DOM event code. It doesn't matter what you want to do with the keycodes, you can continue to use the same directive.

    <div ng-key-down="keyCode = $keyCode">{{keyCode}}</div>
    <div ng-key-down="alert($keyCode)"></div>
    <div ng-key-down="higherLevelControllerFunction($keyCode)"></div>

All of these are allowed, and they will even work simultaneously on the same page ( you need to click between the different elements to activate different ones)

Additionally, testing is way easier. See if you can write a test for this directive.



--
You received this message because you are subscribed to the Google Groups "AngularJS" group.
To unsubscribe from this group and stop receiving emails from it, send an email to angular+u...@googlegroups.com.
To post to this group, send email to ang...@googlegroups.com.
Visit this group at http://groups.google.com/group/angular.
For more options, visit https://groups.google.com/groups/opt_out.



--
chrisrhoden

Arni Arent

unread,
Oct 17, 2013, 4:42:04 PM10/17/13
to ang...@googlegroups.com
Thanks a lot for your explanation, very helpful.
Reply all
Reply to author
Forward
0 new messages