Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.
    }
  }
}
like image 489
Coder1 Avatar asked Aug 15 '13 02:08

Coder1


3 Answers

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.
like image 149
Ian Haggerty Avatar answered Oct 21 '22 02:10

Ian Haggerty


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.

like image 39
Ivan V. Avatar answered Oct 21 '22 03:10

Ivan V.


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:

  • http://toddmotto.com/all-about-angulars-emit-broadcast-on-publish-subscribing/

  • http://nathanleclaire.com/blog/2014/04/19/5-angularjs-antipatterns-and-pitfalls/

like image 3
wesww Avatar answered Oct 21 '22 03:10

wesww