18

I'm trying to generate an n-level hierarchical unordered list with anugularJS, and have been able to successfully do so. But now, I'm having scope issues between the directive and controller. I need to change a scope property of the parent from within a function called via ng-click in the directive template.

See http://jsfiddle.net/ahonaker/ADukg/2046/ - here's the JS

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

//myApp.directive('myDirective', function() {});
//myApp.factory('myService', function() {});

function MyCtrl($scope) {
    $scope.itemselected = "None";
    $scope.organizations = {
        "_id": "SEC Power Generation",
        "Entity": "OPUNITS",
        "EntityIDAttribute": "OPUNIT_SEQ_ID",
        "EntityID": 2,
        "descendants": ["Eastern Conf Business Unit", "Western Conf Business Unit", "Atlanta", "Sewanee"],
        children: [{
            "_id": "Eastern Conf Business Unit",
            "Entity": "",
            "EntityIDAttribute": "",
            "EntityID": null,
            "parent": "SEC Power Generation",
            "descendants": ["Lexington", "Columbia", "Knoxville", "Nashville"],
            children: [{
                "_id": "Lexington",
                "Entity": "OPUNITS",
                "EntityIDAttribute": "OPUNIT_SEQ_ID",
                "EntityID": 10,
                "parent": "Eastern Conf Business Unit"
            }, {
                "_id": "Columbia",
                "Entity": "OPUNITS",
                "EntityIDAttribute": "OPUNIT_SEQ_ID",
                "EntityID": 12,
                "parent": "Eastern Conf Business Unit"
            }, {
                "_id": "Knoxville",
                "Entity": "OPUNITS",
                "EntityIDAttribute": "OPUNIT_SEQ_ID",
                "EntityID": 14,
                "parent": "Eastern Conf Business Unit"
            }, {
                "_id": "Nashville",
                "Entity": "OPUNITS",
                "EntityIDAttribute": "OPUNIT_SEQ_ID",
                "EntityID": 4,
                "parent": "Eastern Conf Business Unit"
            }]
        }]
    };

    $scope.itemSelect = function (ID) {
        $scope.itemselected = ID;
    }
}

app.directive('navtree', function () {
    return {
        template: '<ul><navtree-node ng-repeat="item in items" item="item" itemselected="itemselected"></navtree-node></ul>',
        restrict: 'E',
        replace: true,
        scope: {
            items: '='
        }
    };
});

app.directive('navtreeNode', function ($compile) {
    return {
        restrict: 'E',
        template: '<li><a ng-click="itemSelect(item._id)">{{item._id}} - {{itemselected}}</a></li>',
        scope: {
            item: "=",
            itemselected: '='
        },
        controller: 'MyCtrl',
        link: function (scope, elm, attrs) {
            if ((angular.isDefined(scope.item.children)) && (scope.item.children.length > 0)) {
                var children = $compile('<navtree items="item.children"></navtree>')(scope);
                elm.append(children);
            }
        }
    };
});

and here's the HTML

<div ng-controller="MyCtrl">
    Selected: {{itemselected}}

    <navtree items="organizations.children"></navtree>
</div>

Note the list is generated from the model. And ng-click calls the function to set the parent scope property (itemselected), but the change only occurs locally. Expected behavior, when I click on an item, is that "Selected: None" should change to "Selected: xxx" where xxx is the item that was clicked.

Am I not binding the property between the parent scope and the directive appropriately? How do I pass the property change to the parent scope?

Hope this is clear.

Thanks in advance for any help.

user2165994
  • 275
  • 2
  • 3
  • 6
  • 1
    Welcome to StackOverflow! It might improve your post if you rephrase it in the form of an actual question near the end. – Sid Holland Mar 13 '13 at 15:20

3 Answers3

18

Please have a look at this working fiddle, http://jsfiddle.net/eeuSv/

What i did was to require the parent controller inside the navtree-node directive, and call a member function defined in that controller. The member function is setSelected. Please note that its this.setSelected and not $scope.setSelected. Then define a navtree-node scope method itemSelect. While you click on the anchor tags, it will call the itemSelect method on the navtree-node scope. This inturn will call the controllers member method setSelected passing the selected id.

scope.itemSelect = function(id){ myGreatParentControler.setSelected(id) }

Rajkamal Subramanian
  • 6,884
  • 4
  • 52
  • 69
  • 1
    Thanks rajkamal....I have it working. I'm still confused on the `require: "^ngController"` Where does ngController come from? – user2165994 Mar 13 '13 at 19:47
  • 3
    to say it in general terms, the `require: "^ngController"` statement looks for the parent elements which has controller defined on it. Since we have defined controller `MyCtrl` above the `navtree`, it is passed to the linking function of the nav-node directive as the fourth parameter. You can read about this in http://docs.angularjs.org/guide/directive under Directive Definition Object heading. Here they have defined the options. – Rajkamal Subramanian Mar 13 '13 at 20:05
  • 3
    +1. Nice. I haven't seen anyone else give an example on SO that used `require: '^ngController'`. All of the other examples I've seen require ngModel or another directive's controller. – Mark Rajcok Mar 13 '13 at 22:26
  • +1 I wish I found this earlier. Being able to bind a directive to a controller with '^ngController' is awesome. Thanks so much @rajkamal! – Julia Anne Jacobs Sep 03 '13 at 19:31
  • 3
    @rajkamal Please post your answer into your answer, instead of just through JSFiddle. If JSFiddle goes down, your answer should retain its relevance. – George Stocker Feb 18 '14 at 17:01
11

Maxdec is right, this has to do with scoping. Unfortunately, this is a case that's complicated enough that the AngularJS docs can be mis-leading for a beginner (like myself).

Warning: brace yourself for me being a little long-winded as I attempt to explain this. If you just want to see the code, go to this JSFiddle. I've also found the egghead.io videos invaluable in learning about AngularJS.

Here's my understanding of the problem: you have a hierarchy of directives (navtree, navitem) and you want to pass information from the navitem "up the tree" to the root controller. AngularJS, like well-written Javascript in general, is set up to be strict about the scope of your variables, so that you don't accidentally mess up other scripts also running on the page.

There's a special syntax (&) in Angular that lets you both create an isolate scope and call a function on the parent scope:

// in your directive
scope: {
   parentFunc: '&'
}

So far so good. Things get tricky when you have multiple levels of directives, because you essentially want to do the following:

  1. Have a function in the root controller that accepts a variable and update the model
  2. A mid-level directive
  3. A child-level directive that can communicate with the root controller

The problem is, the child-level directive cannot see the root controller. My understanding is that you have to set up a "chain" in your directive structure that acts as follows:

First: Have a function in your root controller that returns a function (which has reference to the root view controller's scope):

$scope.selectFunctionRoot = function () {
    return function (ID) {
        $scope.itemselected = ID;
    }
}

Second: Set up the mid-level directive to have it's own select function (which it will pass to the child) that returns something like the following. Notice how we have to save off the scope of the mid-level directive, because when this code is actually executed, it will be in the context of the child-level directive:

// in the link function of the mid-level directive. the 'navtreelist'
scope.selectFunctionMid = function () {
    // if we don't capture our mid-level scope, then when we call the function in the navtreeNode it won't be able to find the mid-level-scope's functions            
    _scope = scope;
    return function (item_id) {
        console.log('mid');
        console.log(item_id);

        // this will be the "root" select function
        parentSelectFunction = _scope.selectFunction();
        parentSelectFunction(item_id);
    };
};

Third: In the child-level directive (navtreeNode) bind a function to ng-click that calls a local function, which will, in turn, "call up the chain" all the way to the root controller:

// in 'navtreeNode' link function
scope.childSelect = function (item_id) {
    console.log('child');
    console.log(item_id);

    // this will be the "mid" select function  
    parentSelectFunction = scope.selectFunction();
    parentSelectFunction(item_id);
};

Here's the updated fork of your JSFiddle, which has comments in the code.

Ben Jacobs
  • 2,526
  • 4
  • 24
  • 34
  • Excellent answer on sharing a common function using a two-way binding of the scope, between controller and directive. Thanks! – Alex Apr 16 '14 at 08:57
3

It may be because each directive creates his own scope (actually you tell them to do so).
You can read more about directives here, especially the chapter "Writing directives (long version)".

scope - If set to:

true - then a new scope will be created for this directive. If multiple directives on the same element request a new scope, only one new scope is created. The new scope rule does not apply for the root of the template since the root of the template always gets a new scope.

{} (object hash) - then a new 'isolate' scope is created. The 'isolate' scope differs from normal scope in that it does not prototypically inherit from the parent scope. This is useful when creating reusable components, which should not accidentally read or modify data in the parent scope.

So the changes you do are not reflected in the MyCtrl scope because each directive has his own 'isolated' scope.

That's why a click only changes the local $scope.itemselected variable and not 'all' of them.

maxdec
  • 5,707
  • 2
  • 30
  • 35
  • Thanks maxdec, but the directive needs its own scope to be able to recursively walk through the hierarchy. How do I get the best of both worlds: local scope for the hierarchy and access to the parent scope for other properties? I've read the documentation you link to ad nauseam and thought I understood, particularly the bit about "= or =attr - set up bi-directional binding between a local scope property and the parent scope property..." & "Any changes to parentModel will be reflected in localModel and any changes in localModel will reflect in parentModel." But I just can't make this work. – user2165994 Mar 13 '13 at 18:21
  • @user2165994, "How do I get the best of both worlds: local scope for the hierarchy and access to the parent scope for other properties?" -- use `scope: true`. Your directive will get it's own scope that [prototypically inherits](http://stackoverflow.com/questions/14049480/what-are-the-nuances-of-scope-prototypal-prototypical-inheritance-in-angularjs/14049482#14049482) from the parent scope. So you can define your own properties, and/or reference parent properties. If you need to write to a parent property, make sure it is an object property (e.g., `model.prop1`), not a primitive (`prop1`). – Mark Rajcok Mar 13 '13 at 22:29