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

I have this directive:

app.directive('recursiveListItem', ['$http', 'RecursionHelper', function ($http, RecursionHelper) {
    return {
        restrict: 'E',
        scope: {
            parent: '=',
            onNodeClick: '&',
        },
        compile: function (element, attributes) {
            return RecursionHelper.compile(element);
        },
        template:
            '<div class="list-group-item-heading text-muted parent "> \
                <input type="checkbox" data-ng-click="visible = !visible" id="{{parent.Name}}">\
                <label for="{{parent.Name}}">&nbsp;&nbsp;</label>\
                <a href="javascript:void(0)" data-ng-click="onNodeClick({node: parent})">{{parent.Name}}</a> \
            </div> \
            <ul data-ng-if="parent.Children.length > 0" data-ng-show="visible"> \
                <li ng-repeat="child in parent.Children"> \
                    <recursive-list-item data-parent="child" data-on-node-click="onNodeClick"></recursive-list-item> \
                </li> \
            </ul>',     
    };
}]);

and here is the helper:

app.factory('RecursionHelper', ['$compile', function ($compile) {
    var RecursionHelper = {
        compile: function (element) {
            var contents = element.contents().remove();
            var compiledContents;
            return function (scope, element) {
                if (!compiledContents) {
                    compiledContents = $compile(contents);
                }
                compiledContents(scope, function (clone) {
                    element.append(clone);
                });
            };
        }
    };

    return RecursionHelper;
}]);

Everything works like a charm, but I don't get my on-node-click to work. For the all the root items the 'callback' works fine, but from there on, all the children won't fire the callback. I think it has something to do with passing the function reference to the next child in the template.

I've also tried data-on-node-click="onNodeClick(node)", but that doesn't work either.

Does someone know how to pass the function reference to the child nodes?

Answers


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/


Need Your Help

What are the practical uses of the new methods ES5 provides?

javascript ecmascript-5

With libraries such as ES5-Shim, we can use the new hotness now. Many methods on arrays (like forEach, map, every, etc) could be used now to write clean and beautiful code.

Check a String matches a Regex

javascript html

I want to use JavaScript to do some client-side validation to check whether a string matches the regex:

About UNIX Resources Network

Original, collect and organize Developers related documents, information and materials, contains jQuery, Html, CSS, MySQL, .NET, ASP.NET, SQL, objective-c, iPhone, Ruby on Rails, C, SQL Server, Ruby, Arrays, Regex, ASP.NET MVC, WPF, XML, Ajax, DataBase, and so on.