1

I would like to build a flyout tree menu that adds an active class to elements on hover, and removes it when any of their siblings are hovered.

This way, the menu will stay open when the user mouses away from it.

The flyout menu is recursive because it could be any number of levels deep.

In order to remove a class from siblings, I must have some knowledge of their parent or sibling status. Would prefer to do this with the data structure, but will use DOM traversal if needed.

I have tried:

  • Using scope.$parent in the hover handler (but the scope the handler knows about is the directive scope, not the ng-repeat scope)
  • Passing the event to the hover handler and calling $(evt).siblings() (but angular yells, saying I cannot access the DOM from an expression)
  • setting a .parent property in the local ng-repeat scope (but the event handler does not have access to these values that the template inherits)
  • Using a recursive directive, so every leaf in the tree is another directive. This explodes the browser. (I think because it tries to compile the directive before looking for the exit condition on the loop)
  • Adding a controller to every leaf in the tree. This makes the scope tree large, and doesn't provide a clean way to access siblings. It is functional though. See my answer below.

I have no idea how to do this in Angular. To do this in jQuery is so simple: $(e.target).siblings().removeClass('active'). How can I get this done?

I've made a Plunker to demonstrate the issue with accessing siblings in a recursive ng-repeat: http://plnkr.co/edit/vBYYt6sTvWKN9TXz9SC5?p=preview

John Slegers
  • 45,213
  • 22
  • 199
  • 169
SimplGy
  • 20,079
  • 15
  • 107
  • 144
  • Have you looked at the docs for [Creating Directives that Communicate](http://code.angularjs.org/1.2.13/docs/guide/directive#creating-custom-directives_demo_creating-directives-that-communicate) ? – JoseM Mar 11 '14 at 01:26
  • Yes. Are you suggesting I create a directive for each node in the tree? Might be work a thought... – SimplGy Mar 11 '14 at 07:03
  • Have you tried CSS :hover pseudo-class? – Stewie Mar 11 '14 at 08:04
  • Yes, I believe that placing that logic in a directive makes more sense – JoseM Mar 11 '14 at 12:46
  • @Stewie `:hover` is how these are normally done, but the UI requirement is that the menu stays open after leaving. – SimplGy Mar 11 '14 at 16:13
  • @JoseM Wow, so, a directive that includes itself absolutely destroys the browser, every time. And my recursion only goes 4 levels deep. :) – SimplGy Mar 11 '14 at 16:30
  • @SimpleAsCouldBe yes I noticed the same thing although there is a way to get recursion to work, see this answer: http://stackoverflow.com/a/19167496/238427 - I abandoned my attempt to do what you want after I spent waaay too much time on it. If you don't use ng-class you should be able to do `$element.parent().children().removeClass('active');$element.addClass('active');` – JoseM Mar 11 '14 at 17:53

3 Answers3

1

A great solution uses a variable to track the active leaf at each level of the tree. In this method one declares a variable in a parent scope and assigns it in the leaf scope. Only one sibling is active at any given time: ng-class="{active: branch.name == activeLeaf}".

Plunkr written up by Cam[tab] from #angularjs: http://plnkr.co/edit/u5EiP2DmQtBROxL7Qq53?p=preview

You could use a controller above each ng-repeat to declare the scope, but that adds lots of scope layers. You could use nested directives to contain the scope, but those recurse infinitely. Cam's solution, linked above, includes the nested directive in an ng-include, which generates a nice lean scope tree and avoids the infinite recursion.

SimplGy
  • 20,079
  • 15
  • 107
  • 144
0

you can use $index while using ng-repeat to give ids to all the siblings and add the following code on the div where you do ng-repeat

ie.

id="$index"

use

ng-class={active: selected==$index}

on click change the value of selected

ng-click="selected=$index"

you can access the index of the parent loops by using $parent.$index from your inner ng-repeat loop

Rishul Matta
  • 3,383
  • 5
  • 23
  • 29
  • I don't see this working. `$index` is not an id, it's a counter. It gives you the iteration of the current loop. If I have a loop over 2 items, each item containing 2 more items, the indexes will print: `0 0 1 1 0 1`. – SimplGy Mar 11 '14 at 16:12
  • so if you can access parent loops index and combine it with the inner loop's index you can uniquely determine elements – Rishul Matta Mar 11 '14 at 16:42
0

The following is not a good answer, but it is functional. I want to provide it so folks see how bad the only functional solution I can find is :)

If you add a controller to every leaf of the recursive tree, and attach the hover or click handler to that controller, you'll have access to the scope at that level in the handler. Because scope provides a $parent method, you can climb up and get the siblings.

This doesn't sound terribly unclean, but wait until you see how many Dante-esque levels you have to climb out of.

Leaf.html:

<a href
 ng-click="onClick($event, branch)"
 ng-controller="LeafController"
 >

{{ branch.name }}
<span ng-if="branch.children">&gt;</span>
<ul ng-if="branch.children">
  <li ng-repeat="branch in branch.children">
    <ng-include src="'leaf.html'"></ng-include>
  </li>
</ul>

LeafController:

angular.module('app').controller('LeafController', function($scope) 
  $scope.onClick = function(evt, leaf) {
    evt.stopPropagation();
    console.log('$scope', $scope); // Should be able to get to siblings now, right?
  };
});

Sibling objects are now accessible at this location (I wish I was joking):

$scope.$parent.$parent.$parent.$parent.$parent.$parent.branch.children

I don't want to rely on this chain. There has to be a better way, right?

SimplGy
  • 20,079
  • 15
  • 107
  • 144