8

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?

0

2 Answers 2

23

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/

Sign up to request clarification or add additional context in comments.

9 Comments

Thanx for the solution and explanation. However, I must say I find it hard to grasp what's really going on (the '&' part, why to change the function name and why to use scope.&parent).
My explanation wasn't the best. I've updated it so I hope it's a bit clearer. The way & works is pretty tricky. And in case it helps scope.onNodeClickFn = scope.$parent.onNodeClickFn just keeps putting copies of onNodeClickFn on the current scope (copying the one on the parent scope down to the current nested scope).
Thanks for your updated answer, appreciate it. It still stays difficult stuff though :)
@b1r3k Here's your plunker updated with this: plnkr.co/edit/gH1b9DkcOZ9T0nXyt3pk?p=preview
The original jsfiddle doesn't seem to be working anymore. The console.log command in the onNodeClickFn does not fire.
|
1

I just doing the same thing with a questionnaire app, and the way that I keep the function passed in isolated scope through the all levels was putting the function in each item in function({param:param}) notation

In your could be as well:

 <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({param:param})"></recursive-list-item> \
            </li> \
        </ul>',  

from the main controller(the page where is being added the directive) onNodeClick has params that will be set into the directive you must specify the name in the on-node-click attribute of the root directive, something like that

on-node-click="onNodeClick(param1,param2)"

and in the directive controller you can invoke it as well:

onNodeClick({param1: value1, param2:value2})

Otherwise if onNodeClick only affects vars in the main controller(the page where is being added the directive) you can pass to directive the name of the function with a param existent in the main controller scope and use it with no params in the directive controller.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.