javascriptangularjsangularjs-directiveangularjs-scopeangularjs-bindings

Broadcast not received in directive


I have a parent-child controller relationship between my <main>, <div> and <my-directive> as such:

<main ng-controller="MainController">

    <nav>
        <ul>
            <li>1</li>
            <li>2</li>
            <li>3</li>
            <li>4</li>
        </ul>
    </nav>

    <div ng-controller="AboutController">

    </div>

    <my-directive></my-directive>

</main>

Within the MainController, i perform a $broadcast with a:

$scope.$broadcast('shoppingCartReady', msg);

Within AboutController, i can successfully receieve this $broadcast via:

angular.module('aboutModule', [])
    .controller('AboutController', require('./about/aboutController'));

var AboutController = function ($scope, $rootScope) {

    $scope.$on('shoppingCartReady', function(event, msg){
        console.log(msg);
    });

};

module.exports = [ '$scope', '$rootScope', AboutController];

However, when trying to receive the $broadcast within <my-directive> link function, it seems to never be received:

angular.module('shopModule', [])
    .directive('shopModuleDirective', require('./shopModuleDirective'))
    .directive('myDirective', require('./myDirective'));

var myDirective = function ($rootScope) {

    function link($scope, element, attrs, baseCtrl) {

        $scope.$on('shoppingCartReady', function (event, msg) {
            console.log(msg);
        });
    }

    return {
        restrict: 'E',
        require: '^shopModuleDirective',
        link: link
    };
};

return ['$rootScope', myDirective];

UPDATE


I have even created a controller within my directive:

angular.module('shopModule', [])
    .directive('shopModuleDirective', require('./shopModuleDirective'))
    .directive('myDirective', require('./myDirective'));

var myDirective = function ($rootScope) {

    var controller = ["$scope", "$element", function ($scope, element) {
        console.log('waiting for .on....');
        $scope.$on('shoppingCartReady', function (event, cache) {
             console.log('shoppingCartReady .on rcvd!');
        });
    }]

    function link($scope, element, attrs, baseCtrl) {

        $scope.$on('shoppingCartReady', function (event, msg) {
            console.log(msg);
        });
    }

    return {
        restrict: 'E',
        require: '^shopModuleDirective',
        link: link
    };
};

return ['$rootScope', myDirective];

I can see the log for

console.log('waiting for .on....');

But the .on is never received.

UPDATE:


I think the possible cause is due to scope's not being 'ready'.

Within my original broadcast from MainController, if i perform another after a setTime out, all .on are received:

MainController:

$scope.$broadcast('shoppingCartReady', msg);

setTimeout(function(){
    $scope.$broadcast('shoppingCartReady', msg);
}, 8000);

Solution

  • There is a race condition.

    myDirective controller and post-link function are executed after MainController, and the listener is set up after shoppingCartReady event has been triggered.

    A rule of thumb for good, testable directive design is to keep all scope-related logic in controller, linking functions and $onInit hook are used exactly for the things that should happen there and not anywhere else, like manipulations on compiled DOM content.

    Less obvious but distinctive use case for link is the desirable order of execution (post-linking functions are executed in reverse order, from children to parent). If MainController becomes a directive, and scope.$broadcast('shoppingCartReady', msg) is executed in link function and not controller, this guarantees the proper order of execution.

    Wrapping controller code into zero $timeout will also postpone the code

    $timeout(function(){
        $scope.$broadcast('shoppingCartReady', msg);
    });
    

    to be executed after linking functions in children directives, but will also designate some code smell. This is one of the reasons why scope events are less desirable ways of directive communication and are prone to be anti-patterns.

    The alternative way of handling this depends on what shoppingCartReady event is about, but in current case it is redundant: it has already happened at the moment when children controllers are executed, they can safely assume that 'shopping cart is ready'.

    A suggested reading for this topic: Directive Controller And Link Timing In AngularJS .