jordancooperman
jordancooperman

Reputation: 2011

Angular $scope variable not updating after being called through $rootScope.$on

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!

Upvotes: 0

Views: 863

Answers (1)

jnthnjns
jnthnjns

Reputation: 8925

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

Upvotes: 1

Related Questions