How to pass a function reference in a recursive directive in AngularJS?

Underlying Issue

As a lead in its helpful to look at '&' which the docs describe this way:

& or &attr - provides a way to execute an expression in the context of the parent scope.

Often it's desirable to pass data from the isolated scope via an expression and to the parent scope, this can be done by passing a map of local variable names and values into the expression wrapper fn. For example, if the expression is increment(amount) then we can specify the amount value by calling the localFn as localFn({amount: 22})

In order to achieve this the function that is passed through an & is wrapped in another function parentGet(). We can see this in action by looking at the contents of the click handler function variable. First, before it's passed into the & it's as we'd expect:

function (node) {
    console.log("called with ",node);
} 

But, then, inside your directive, after passing through '&', it looks like this:

function (locals) {
     return parentGet(scope, locals);
} 

So instead of a direct reference to your click handler function it is now a call that will apply your function using both the parent scope and any locals which you pass in.

The problem is that, from what I can tell, as we nest down that scope variable keeps getting updated to the new nested parent scope. That's not good for us. We want the execution context to be that top scope where your click handler is. Otherwise we lose reference to your function- as we see happening.

Solution

To fix this we keep a reference to the original function (that isn't encumbered by parentGet() as we nest down). Then when we pass it in, the scope used in parentGet() is the one with click handler function on it.

First let's use a different name for the actual function than you'll use for the parameter. I set up the function like so $scope.onNodeClickFn = function(node) {...}

Then we make 3 changes:

1) Add a second scope variable (topFunc):

scope: {
          onNodeClick: '&',
          topFunc: '='
       }

onNodeClick is the wrapped function (including scope parameter). And topFunc is a reference to the unwrapped function (note that we pass that in using = so it's a reference to the function absent the wrapper that & applies).

2) At the top level, we pass both function references in.

<recursive-list-item on-node-click="onNodeClickFn(node)" top-func="onNodeClickFn" parent=parent ></recursive-list-item>

(note the lack of () on the top-func parameter.)

3) Pass the new topFunc parameter in to the template:

<recursive-list-item data-parent="child" data-on-node-click="topFunc(node)" top-func="topFunc"></recursive-list-item> \

So we are now maintaining a reference, at each nested scope, to the original function and using that in the template.

You can see it working in this fiddle: http://jsfiddle.net/uf6Dn/9/