3

I have created an accordian directive in Angular which can be nested, so an accordian can have child accordians inside them.

I want to broadast an event when the accordian opens and closes so that other directives can listen for it (e.g. a menu inside an accordian panel might reveal itself when the accordian it's inside is opened).

The thing is that if there is a nested inner accordian inside the outer one I don't want the event to be broadcast to the inner accordian's child elements because the inner accordian hasn't broadcast an open/close event.

Just in case that makes no sense, to put it another way an element inside a nested accordian should be able to listen to an open/close event broadcast by the accordian it is in, not the one further up the DOM tree.

Hopefully there is a simple solution to this.

Update: added demo here: http://jsfiddle.net/jonhobbs/xr1kLqba/

I clearly haven't understood $broadcast and $on properly as the demo events currently aren't working at all, but it should be clear what I'm trying to do!

Edit: Apparently all links to jsfiddle must be accompanied by code, so here is some.

var app = angular.module('app', []);

app.controller('MainCtrl', function($scope) {
  $scope.mainMenuOpen = true;
})

.directive('myPanel', function() {
  return {
    restrict: 'EA',
    scope: {
      isOpen: '=?'
    },
    link: function($scope, $element, $attrs) {  
      $element.find('> BUTTON').bind('click', function() {

        $scope.isOpen = !$scope.isOpen;
        if ($scope.isOpen) {
          $scope.$broadcast('panelOpened');
        }
      });
    }
  };
})

.directive('reactToPanelOpenClose', function() {
  return {
    restrict: 'EA',
    scope: {},
    link: function($scope, $element, $attrs) {
      $scope.$on('panelOpened', function() {

        alert("clicked");

        $element.css({ 'background-color': 'red' });
      });
    }
  };
});
New Dev
  • 48,427
  • 12
  • 87
  • 129
jonhobbs
  • 26,684
  • 35
  • 115
  • 170
  • a bit of code helps here, such as what your events are named, which scope you are broadcasting from / listening to, etc.... – Claies May 03 '15 at 22:08
  • Sorry, there's probably a bit too much code to post here and cutting it down to make a jsfiddle would be a challenge. I can call the events anything I want as they currently don't exist and assume i'll be broadcasting them from the accordian directive I've created. To be honest it doesn't need to be about my accordian directive it's more of a general question about hot to broadcast an event down the chain but stop when you get to a directive of the same type. – jonhobbs May 03 '15 at 22:13
  • well, to be honest, since `$broadcast` can't be canceled by recipients, the scenario you have just described isn't easily modeled. The easiest way to do this would probably be to programmatically generate the string key that is used for your event name and somehow pass that value to the elements that need to listen, so that every accordion is unique. – Claies May 03 '15 at 22:33
  • Thanks, that makes sense. I had a feeling that $broadcast wouldn't be able to be cancelled. I'm currently exploring the idea of using require:^accordian on the child elements and having them register a callback or something with the accordian instead of using $broadcast/$on – jonhobbs May 03 '15 at 22:36
  • Issue sounds relatively complex when reading this, but solution is likely relatively simple. I really don't see how it would take very long to mock this up in a simplified demo that wouldn't take very long to create in order to better understand what you are working with ... garbage in, garbage out – charlietfl May 03 '15 at 23:52
  • OK, I've added a jsfiddle. – jonhobbs May 04 '15 at 01:30

3 Answers3

1

By far, the easiest approach is to use require as you alluded to in the comments.

One way to address this is to register children controllers (or even their callback functions), and then the parent can call into each child instead of broadcasting.

The thing to note here is that you would need to require: "?^^parent" - meaning, make the require optional and strictly-ancestor selector; for "^" Angular would return self.

.directive("accordion", function(){
  return {
    require: ["accordion", "?^^accordion"],
    controller: function(){
       var children = [];
       this.registerChild = function(childCtrl){
          children.push(childCtrl);
       }

       this.handleNotification = function(){
          // do something when notification arrives from parent
       }

       this.notify = function(){
          angular.forEach(children, function(child){
            child.handleNotification();
          })
       }
    },
    link: function(scope, element, attrs, ctrls){
      var me = ctrls[0], parent = ctrls[1];

      if (parent){
         parent.registerChild(me);
      } 
    }
  }
})
New Dev
  • 48,427
  • 12
  • 87
  • 129
  • Thanks, I think i can adapt this code and make it work. It's not actually the sub-accordians that need to react when the accordian opens, it's another directive entirely which needs to require it and register a callback so I don't have to worry about the "self" issue, I just need to put the registerChild and notify functions on the accordian and the handleNotification on the other directive which is registering itself with the accordian. – jonhobbs May 04 '15 at 12:33
1

Yo're trying to work with angular context inside of jQuery callback, so that's will not work. The easiest approach to solve your problem is the following code snippet:

angular.$externalBroadcast = function (element, event, data) {
    var scope = element.scope();
    scope.$apply(function () {
        scope.$broadcast(event, data);
    });
};

And apply this function inside link function of your panel directive:

$scope.isOpen = !$scope.isOpen;                
if($scope.isOpen){
    angular.$externalBroadcast($element, 'panelOpened');
}

Have a look at this modified Fiddle.

ChernikovP
  • 471
  • 1
  • 8
  • 18
  • Thanks, but why doesn't it work when you just wrap the call to $broadcast in a $scope.$apply() , why do you need to create an external function? – jonhobbs May 04 '15 at 12:14
  • Actually it does work if you do this.... $scope.$apply(function () { $element.scope().$broadcast('panelOpened'); }); ... but doens't if you use $scope.$broadcast() , it must be $element.scope().$broadcast() – jonhobbs May 04 '15 at 12:28
0

You could add a number value depth or similar to your broadcast, that defines the hierarchical depth the broadcast was sent from. if the directives controller listening, has a depth of 1 higher, it could execute. Else it could just ignore the broadcast.

If it's unclear, I could also add a small example, just let me know.

Rias
  • 1,956
  • 22
  • 33