Coder1
Coder1

Reputation: 13321

Communicating with sibling directives

Goal: Create behaviors using directives with communication between 2 sibling elements (each their own directive).

A behavior to use in example: The article content is hidden by default. When the title is clicked, I want the related article content to display.

The catch: The related article elements need to associate to each other without being nested in a single parent element or directive.

<div article="article1">this is my header</div>
<div id="article1" article-content>this is content for the header above</div>

<div article="article2">this is my header</div>
<div id="article2" article-content>this is content for the header above</div>

I know it would be easier to place the content inside the article directive, however this question is to find out how to solve a situation like this.

Can the content directive pass itself to the related article directive somehow?

This code isn't very useful as it is now, but it's a starting point. How would I accomplish this?

.directive('article', function(){
  return {
    restrict: "A",
    controller: function($scope) {
      $scope.contentElement = null;
      this.setContentElement = function(element) {
        $scope.contentElement = element;
      }
    },
    link: function(scope, element) {
      element.bind('click', function(){
        // Show article-content directives that belong
        // to this instance (article1) of the directive
      }
    }
  }
}
.directive('articleContent', function(){
  return {
    require: "article",
    link: function(scope, element, attrs, articleCtrl) {
      // Maybe reference the article i belong to and assign element to it?
      // I can't though because these are siblings.
    }
  }
}

Upvotes: 23

Views: 17437

Answers (6)

Ivan V.
Ivan V.

Reputation: 8101

Or you can create a service just for directive communication, one advantage of special service vs require is that your directives won't depend on their location in html structure.

Upvotes: 10

wesww
wesww

Reputation: 2873

The above solutions are great, and you should definitely consider using a parent scope to allow communication between your directives. However, if your implementation is fairly simple there's an easy method built into Angular that can communicate between two sibling scopes without using any parent: $emit, $broadcast, and $on.

Say for example you have a pretty simple app hierarchy with a navbar search box that taps into a complex service, and you need that service to broadcast the results out to various other directives on the page. One way to do that would be like this:

in the search service

$rootScope.$emit('mySearchResultsDone', {
  someData: 'myData'
}); 

in some other directives/controllers

$rootScope.$on('mySearchResultsDone', function(event, data) {
  vm.results = data;
});

There's a certain beauty to how simple that code is. However, it's important to keep in mind that emit/on/broadcast logic can get nasty very quickly if you have have a bunch of different places broadcasting and listening. A quick google search can turn up a lot of opinions about when it is and isn't an anti-pattern.

Some good insight on emit/broadcast/on in these posts:

Upvotes: 3

Ian Haggerty
Ian Haggerty

Reputation: 1751

None of the directive require options will allow you to require sibling directives (as far as I know). You can only:

  • require on the element, using require: "directiveName"
  • tell angular to search up the DOM tree using require: "^directiveName"
  • or require: "^?directiveName" if you don't necessarily need the parent controller
  • or require: "^\?directiveName" if you don't necessarily need the parent DOM wrapper

If you want sibling to sibling communication, you'll have to house them in some parent DOM element with a directive controller acting as an API for their communication. How this is implemented is largely dependent on the context at hand.

Here is a good example from Angular JS (O Reilly)

app.directive('accordion', function() {
  return {
    restrict: 'EA',
    replace: true,
    transclude: true,
    template: '<div class="accordion" ng-transclude></div>',
    controller: function() {

      var expanders = [];

      this.gotOpened = function(selectedExpander) {
        angular.forEach(expanders, function(expander) {
          if(selectedExpander != expander) {
            expander.showMe = false;
          }
        });
      };

      this.addExpander = function(expander) {
        expanders.push(expander);
      }

    }
  }
});

app.directive('expander', function() {
  return {
    restrict: 'EA',
    replace: true,
    transclude: true,
    require: '^?accordion',
    scope: { title:'@' },
    template: '<div class="expander">\n  <div class="title" ng-click="toggle()">{{ title }}</div>\n  <div class="body" ng-show="showMe" \n       ng-animate="{ show: \'animated flipInX\' }"\n ng-transclude></div>\n</div>',
    link: function(scope, element, attrs, accordionController) {
      scope.showMe = false;
      accordionController.addExpander(scope);

      scope.toggle = function toggle() {
        scope.showMe = !scope.showMe;
        accordionController.gotOpened(scope);
      }
    }
  }
})

Usage (jade templating):

accordion
    expander(title="An expander") Woohoo! You can see mme
    expander(title="Hidden") I was hidden!
    expander(title="Stop Work") Seriously, I am going to stop working now.

Upvotes: 32

Maccurt
Maccurt

Reputation: 13817

I had the same issue with a select all/ select item directive I was writing. My issue was the select all check box was in a table header row and the select item was in the table body. I got around it by implementing a pub/sub notification service so the directives could talk to each other. This way my directive did not care about how my htlm was structured. I really wanted to use the require property, but using a service worked just as well.

Upvotes: 0

user12121234
user12121234

Reputation: 2569

I had the exact same problem and I was able to solve it.

In order to get one directive to hide other sibling directives, I used a parent directive to act as the API. One child directive tells the parent it wasn't to be shown/hidden by passing a reference to its element, and the other child calls the parent toggle function.

http://plnkr.co/edit/ZCNEoh

app.directive("parentapi", function() {
  return {
    restrict: "E",
    scope: {},
    controller: function($scope) {
      $scope.elements = [];

      var on = true;
      this.toggleElements = function() {
        if(on) {
          on = false;
          _.each($scope.elements, function(el) {
            $(el).hide();
          });
        } else {
          on = true;
          _.each($scope.elements, function(el) {
            $(el).show();
          });
        }
      }

      this.addElement = function(el) {
        $scope.elements.push(el);
      }
    }
  }
});

app.directive("kidtoggle", function() {
  return {
    restrict: "A",
    require: "^parentapi",
    link: function(scope, element, attrs, ctrl) {
      element.bind('click', function() {
        ctrl.toggleElements();  
      });
    }
  }
});

app.directive("kidhide", function() {
  return {
    restrict: "A",
    require: "^parentapi",
    link: function(scope, element, attrs, ctrl) {
      ctrl.addElement(element);
    }
  }  
});

Upvotes: 0

Chandermani
Chandermani

Reputation: 42669

If there is a list of articles and its content we can do it without any directive, using ng-repeat

<div ng-repeat="article in articles">
   <div article="article1" ng-click='showContent=true'>{{article.header}}</div>
   <div id="article1" article-content ng-show='showContent'>{{article.content}}</div>
</div>

So you need to define the article model in controller. We are making use of local scope created by ng-repeat.

Update: Based on your feedback, you need to link them together.You can try

<div article="article1" content='article1'>this is my header</div>
<div id="article1" article-content>this is content for the header above</div>

and in your directive

use

link: function(scope, element,attrs) {
      element.bind('click', function(){
        $('#'+attrs.content).show();
      }
    }

And the final method could be to use $rootScope.$broadcast and scope.$on methods to communicate between to controllers. But in this approach you need to track from where the message came and who is the intended recipient who needs to process it.

Upvotes: 0

Related Questions