Rotem B
Rotem B

Reputation: 1377

Dependencies between angular directives

I've created a small app that has 2 directives: One that adds a google map div and initialize it, and the second, which shows layers that contain markers. The directives are independent, but I want them to communicate: The layers directive is independent, yet it needs to use the google maps directive to add markers to the map. I used $broadcast through $rootScope to communicate between the directives.

The directives are defined as follows:

angular.module('googleMapModule', [])
       .directive('myGoogleMap', function(){
         template: '<div id="map" />',
         controller: function($scope){
             // some initializations

             // Listen for event fired when markers are added
             $scope.$on('addMarkersEvent', function(e, data){
                 // do something
             }
         }
 }


 angular.module('layersDirective', [])
        .directive('myLayers', function() {
           templateUrl: 'someLayersHtml.html',  
           controller: function($http, $scope, $rootScope){
               // Get layers of markers, etc.

               // On specific layer click, get markers and:
                $rootScope.broadcast('addMarkersEvent', {
                   data: myMarkers 
                });
           }
        });

After this long prologue here are my questions:

How should the connection between the two directives be implemented? Is it correct to use $rootScope and $broadcast or should there be a dependency between the myLayers directive and the myGoogleMap directive?

Furthermore, I've read about when to use controller, link and compile, yet I don't know the right way to use them here. My guess is that myGoogleMap should define its API in a controller and that myLayers should be dependent on myGoogleMap.

The example I wrote here works fine in my application. I'm looking for guidance on how to do it right and to understand what I did wrong here.

Thanks, Rotem

Upvotes: 3

Views: 3777

Answers (4)

Chandermani
Chandermani

Reputation: 42669

You have already highlight one may for the directives to communicate using rootscope.

Another way directive can communicate if they are defined on the same html hierarchy is by use directive controller function. You have highlighted that too in your question. The way it would be done is (assuming myGoogleMap is defined on parent html), the two directive definitions become:

angular.module('googleMapModule', [])
    .directive('myGoogleMap', function () {
    template: '<div id="map" />',
    controller: function ($scope) {
        this.addMarkersEvent = function (data) {}
        // some initializations
    }


    angular.module('layersDirective', [])
        .directive('myLayers', function ($http, $rootScope) {
        templateUrl: 'someLayersHtml.html',
        require: '^myGoogleMap',

        link: function (scope, element, attrs, myGoogleMapController) {
            $scope.doWork = function () {
                myGoogleMapController.addMarkersEvent(data);
            }
        }
    });

Here you use the require property on the child directive. Also instead of using child controller all the functionality in the child directive is now added to link function. This is because the child link function has access to parent directive controller.

Just a side note, add both directives to a single module (Update: Actually you can have the directives in different modules, as long as there are being referenced in the main app module.)

Upvotes: 1

Nikos Paraskevopoulos
Nikos Paraskevopoulos

Reputation: 40328

There are some ways for directives to cooperate/communicate

  1. If one is a sibling or child of the other, you can use require. E.g. for this template:

    <dir1>
        <dir2></dir2>
    </dir1>
    

    Use this code:

    app.directive('dir1', function() {
        return {
            ...
            controller: function() {
                // define api here
            }
        };
    });
    app.directive('dir2', function() {
        return {
            ...
            require: '^dir1',
            link: function(scope, elem, attrs, dir1Controller) {
                // use dir1 api here
            }
        };
    });
    
  2. A service, used by both directives to communicate. This is easy and works well if the directives can only be instantiated once per view.

  3. Using $broadcast/$emit on the $rootScope (there is a slight difference: $broadcast will "flood" the scope hierarchy, possibly affecting performance; $emit will only call listeners on the $rootScope, but this means you have to do $rootScope.$on() and then remember to deregister the listener when the current scope is destroyed - which means more code). This approach is good for decoupling components. It may become tricky in debugging, i.e. to find where that event came from (as with all event-based systems).

  4. Other

controller, link and compile

Very short:

  • Use the controller to define the API of a directive, and preferably to define most its logic. Remember the element, the attributes and any transclude function are available to the controller as $element, $attrs and $transclude respectively. So the controller can, in most cases, replace the link function. Also remember that, unlike the link function, the controller is elligible for dependency injection. (However you can still do dependency injection at the directive level, so, after all, the link function can also access dependencies.)

  • Use the link function to access required controllers (see case 1 above). Or, if you are feeling lazy, to define the directive's logic. I think the controller is cleaner though.

  • Use the compile function... rarely :) When you need very special transformations to the template (repetition is the first thing that comes to mind - see ng-repeat) or other mystical stuff. I use directives all the time, about 1% of them needs a compile function.

My guess is that myGoogleMap should define its API in the controller and that myLayers should be dependent on myGoogleMap

The question is how will you communicate this API using events? You probably need only to create an API in a custom event object. The listeners of your event will be using that custom event. If so, the controller does not really need to define an API.


As a bottom line, I am perfectly OK with using events in your case.

Upvotes: 4

Josh
Josh

Reputation: 44916

Generally communication between directives should be handled via controllers and using the require property on the directive definition object.

If we re-work your first directive we can simply add that method to the controller:

directive('myGoogleMap', function () {
    return {
        template: '<div id="map" />',
        controller: function ($scope) {
            var _this = this;

            //Part of this directives API
            _this.addMarkers = function(arg){
                //Do stuff
            }
        }
    };
});

Now we can require this controller in another directive, but one little known features is that you can actually require an array of directives. One of those directives can even be yourself.

All of them will be passed, in order, as an array to your link function:

directive('myLayers', function () {
    return {
        templateUrl: 'someLayersHtml.html',
        controller: function ($http, $scope, $rootScore) {
            // Some get layers of markers functionality
        },
        // Require ourselves
        // The '?' makes it optional
        require: ['myLayers', '?myGoogleMap'],
        link: function(scope, elem, attrs, ctrls){
            var myLayersCtrl = ctrls[0];
            var myGoogleMapCtrl = ctrls[1];

            //something happens
            if(myGoogleMapCtrl) {
                myGoogleMapCtrl.addMarkers(markers);
            }
        }
    };
});

Now you can communicate explicitly opt-in by using the ? which makes the controller optional.

Upvotes: 2

Michel Tom&#233;
Michel Tom&#233;

Reputation: 389

In order for that to work, you have to define both directives in the same module, i.e.:

var module = angular.module('myModule');
module.directive('myGoogleMap', function(){
     template: '<div id="map" />',
     controller: function($scope){
         // some initializations

         // Listen to event for adding markers
         $scope.$on('addMarkersEvent', function(e, data){
             // do something
         }
     }
 }

 module.directive('myLayers', function() {
       templateUrl: 'someLayersHtml.html',  
       controller: function($http, $scope, $rootScore){
       // Some get layers of markers functionality

       // On specific layer click, get markers and:
       $rootScope.broadcast('addMarkersEvent', {
           data: myMarkers 
       });
   }
 });

Read more here.

EDIT: Sorry i didn't understand your question, but according to your comment, quoting from the AngularJs Best Practices:

  • Only use .$broadcast(), .$emit() and .$on() for atomic events that are relevant globally across the entire app (such as a user authenticating or the app closing). If you want events specific to modules, services or widgets you should consider Services, Directive Controllers, or 3rd Party Libs
    • $scope.$watch() should replace the need for events
    • Injecting services and calling methods directly is also useful for direct communication
    • Directives are able to directly communicate with each other through directive-controllers

Upvotes: 1

Related Questions