0

I have a global method for watching keypresses throughout my application. When a keypress happens, I broadcast the event like so:

var keycodes = {
    'escape': 27,
}

angular.element($window).on('keydown', function(e) {
    for (var name in keycodes) {
        if (e.keyCode === keycodes[name]) {
            $rootScope.$broadcast('keydown-' + name, angular.element(e.target))
        }
    }
})

This all works fine. In my controller, I listen for the event like so:

$rootScope.$on('keydown-escape', $scope.hideOverlays)

This also works fine, however, when I try to update attributes on the $scope object, I do not see the correct behavior in the DOM:

 $scope.hideOverlays = function() {
        for (var i=0; i < $scope.gallery.items.length; i++) {
            $scope.gallery.items[i].overlayShown = false;
            $scope.gallery.items[i].editing = false;
        }
    }

When I call this method internally from the controller, everything works fine, so I'm wondering if there's something that Angular is doing differently based on how the method is being called. I've tried called $scope.$apply(). In addition to seeming like that's not the right thing to do, I also get an error, so no dice there. Any help is greatly appreciated!

jordancooperman
  • 1,931
  • 2
  • 21
  • 33
  • Is your `keydown` event in `app.run`? Are you injecting everything properly? [Here is a sample fiddle of your code working](http://jsfiddle.net/7h61dv1L/1/). In the fiddle, make sure to click into the html section before pressing escape so that panel has focus. – jnthnjns Oct 08 '15 at 15:35

1 Answers1

1

There is a lot of missing code that brings up a few questions, where are you executing your keydown event for example, in my example you will see that I have put it in the app.run in Angular:

// Note you have to inject $rootScope and $window
app.run(function ($rootScope, $window) {
    angular.element($window).on('keydown', function (e) {
        for (var name in keycodes) {
            if (e.keyCode === keycodes[name]) {
                $rootScope.$broadcast('keydown-' + name, angular.element(e.target))
            }
        }
    })
});

This allows it to execute before any controllers are loaded.


Then in your controller, instead of running $scope.$apply you should do a "safe" $scope.$apply method that checks for an existing digest or apply phase, if there is one then we shouldn't apply, other wise we can.

// Again make sure that you have everything you need injected properly.
// I need $rootScope injected so I can check the broadcast
app.controller('MainCtrl', ['$scope', '$rootScope', function ($scope, $rootScope) {
    $scope.hello = "Hello World";
    var count = 0;
    $scope.hideOverlays = function () {
        count++;
        $scope.safeApply(function () {
            $scope.hello = "You pressed the escape key " + count + " times.";
        });

    };

    $rootScope.$on('keydown-escape', $scope.hideOverlays);

    $scope.safeApply = function (fn) {
        var phase = this.$root.$$phase;
        // If AngularJS is currently in the digest or apply phase
        // we will just invoke the function passed in
        if (phase == '$apply' || phase == '$digest') {
            // Let's check to make sure a function was passed
            if (fn && (typeof (fn) === 'function')) {
                // Invoke the function passed in
                fn();
            }
        } else {
            // If there is no apply or digest in the phase
            // we will just call $scope.$apply
            this.$apply(fn);
        }
    };
}]);

Here is a working fiddle showing DOM updates when the escape key is pressed

jnthnjns
  • 8,962
  • 4
  • 42
  • 65
  • Thanks! This worked, though I'm not exactly sure why. I was already calling the event from app.run so no difference there, but the safeApply method seems to have done the trick. Mind explaining exactly what's happening there? – jordancooperman Oct 08 '15 at 21:08
  • Also, what's the best way to make this a globally available method? – jordancooperman Oct 08 '15 at 22:11
  • @jordancooperman `safeApply` really only checks to see if there is an active digest or apply being run by Angluar, it prevents the $scope.$apply error because if that phase is active to just invokes the function, otherwise it will simply invoke `$scope.$apply`. Check out the comments I added. – jnthnjns Oct 09 '15 at 14:41
  • @jordancooperman When you say that you want to make the method global are you talking about the `hideOverlays`? If so, you could run it through a service, then you'd just inject that service in each controller. This allows you to only have 1 `hideOverlays` function. [This might help](http://stackoverflow.com/a/15026440/1134705) – jnthnjns Oct 09 '15 at 14:45
  • 1
    @jordancooperman [Here is a fiddle showing the safeApply as a service which allows you to use it globally.](http://jsfiddle.net/7h61dv1L/4/) – jnthnjns Oct 09 '15 at 15:23
  • Got it, I'm surprised that works. The error that I was getting was saying that Angular was in the process of applying, which I would have thought lead this safeApply method to do nothing. In any case, it does work. And that fiddle was exactly what I needed. Thanks so much for your help! – jordancooperman Oct 09 '15 at 20:25