如何在AngularJS中正确地绑定指令和控制器之间的作用域

18

我正在尝试使用AngularJS生成一个n级层次无序列表,已经成功实现。但是,我在指令和控制器之间遇到了作用域问题。我需要在指令模板中通过ng-click调用的函数内更改父级别的作用域属性。

请参见http://jsfiddle.net/ahonaker/ADukg/2046/ - 下面是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);
            }
        }
    };
});

这里是HTML代码

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

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

请注意,该列表是从模型生成的。ng-click调用函数来设置父作用域属性(itemselected),但更改仅在本地发生。预期的行为是,当我点击某个项时,“Selected: None” 应更改为“Selected: xxx”,其中xxx是被单击的项目。

我是否没有适当地将父作用域和指令之间的属性绑定起来?我如何将属性更改传递给父作用域?

希望这很清楚。

提前感谢任何帮助。


1
欢迎来到StackOverflow!如果您在结尾附近以实际问题的形式重新表述,可能会改善您的帖子。 - Sid Holland
3个回答

18
请查看这个可工作的 fiddle,http://jsfiddle.net/eeuSv/ 我所做的是在 navtree-node 指令中要求父控制器,并调用该控制器中定义的成员函数。 该成员函数是 setSelected。请注意,它是 this.setSelected 而不是 $scope.setSelected。 然后定义一个名为 itemSelectnavtree-node 作用域方法。当单击锚标签时,它将调用 navtree-node 作用域上的 itemSelect 方法。 这将反过来调用控制器的成员方法 setSelected 并传入选定的 id。 scope.itemSelect = function(id){ myGreatParentControler.setSelected(id) }

1
谢谢rajkamal...我已经让它工作了。我仍然对require: "^ngController"感到困惑,ngController从哪里来? - user2165994
3
通俗易懂地说,“require: "^ngController"”语句查找具有控制器定义的父元素。由于我们在“navtree”上方定义了控制器“MyCtrl”,因此它被传递给“nav-node”指令的链接函数作为第四个参数。您可以在http://docs.angularjs.org/guide/directive下的指令定义对象标题下了解更多信息。在那里,他们定义了选项。 - Rajkamal Subramanian
3
+1,我还没有看到其他人在SO上使用require: '^ngController'的例子。我看到的所有其他例子都需要ngModel或另一个指令的控制器。 - Mark Rajcok
+1 我希望我早点发现这个。使用'^ngController'将指令绑定到控制器是很棒的。非常感谢@rajkamal! - Julia Anne Jacobs
3
@rajkamal,请将你的答案发布在答案中,而不仅仅是通过JSFiddle。如果JSFiddle崩溃了,你的答案应该仍然具有相关性。 - George Stocker

11

Maxdec是对的,这与作用域有关。不幸的是,这是一个复杂的案例,对于初学者(比如我自己)来说,AngularJS文档可能会误导。

警告:请做好心理准备,因为我将尝试解释这个问题。如果您只想看代码,请转到JSFiddle。我还发现egghead.io视频在学习AngularJS方面非常有价值。

以下是我的理解:您有一组指令(navtree、navitem),您希望将信息从navitem“向上”传递到根控制器。像良好编写的JavaScript一样,AngularJS被设置为严格控制变量的范围,以便您不会意外地破坏页面上运行的其他脚本。

在Angular中有一种特殊语法(&),它可以让您创建隔离作用域并调用父作用域中的函数:

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

So far so good. 当你有多层指令时,情况会变得棘手,因为你基本上想要做到以下几点:
1. 在根控制器中有一个接受变量并更新模型的函数。 2. 一个中间层指令。 3. 一个能够与根控制器通信的子级指令。
问题在于,子级指令无法看到根控制器。我的理解是,你必须在指令结构中设置一个“链”,其作用如下:
首先,在你的根控制器中有一个返回函数的函数(该函数具有对根视图控制器作用域的引用)。
$scope.selectFunctionRoot = function () {
    return function (ID) {
        $scope.itemselected = ID;
    }
}

第二步:设置中级指令的选择功能(它将传递给子级),返回类似以下内容的内容。请注意,我们必须保存中级指令的作用域,因为当此代码实际执行时,它将在子级指令的上下文中执行:

// 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);
    };
};

第三步:在子级指令(navtreeNode)中绑定一个函数到ng-click,调用一个本地函数,该函数将依次“向上调用链”,一直到根控制器:

// 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);
};

这是更新后的 你的JSFiddle分支,其中包含了代码注释。

在共享一个常用函数的问题上,使用控制器和指令之间的双向绑定的方法给出了一个极好的答案。谢谢! - Alex

3
可能是因为每个指令都创建了自己的作用域(实际上您让它们这样做)。
您可以在这里阅读更多关于指令的内容,特别是“编写指令(长版)”章节。

scope - 如果设置为:

true - 那么将为此指令创建一个新的作用域。如果同一元素上有多个指令请求新的作用域,则只会创建一个新作用域。新作用域规则不适用于模板的根部,因为模板的根部总是获得新作用域。

{} (对象哈希) - 然后创建一个新的“隔离”作用域。'isolate' 作用域与普通作用域的区别在于它不会从父作用域原型继承。这在创建可重用组件时很有用,它们不应意外读取或修改父作用域中的数据。

因此,您所做的更改不会反映在MyCtrl作用域中,因为每个指令都有自己的“隔离”作用域。
这就是为什么单击只会更改本地的$scope.itemselected变量,而不是所有变量的原因。

谢谢maxdec,但是指令需要自己的作用域才能递归地遍历层次结构。我如何兼顾两者:对于层次结构使用本地作用域,同时可以访问父级作用域中的其他属性?我已经详细阅读了您提供的链接,特别是“=或=attr-在本地作用域属性和父作用域属性之间建立双向绑定...”,以及“对parentModel的任何更改都将反映在localModel中,而对localModel的任何更改都将反映在parentModel中。”但我就是无法使其工作。 - user2165994
@user2165994,“如何兼顾局部作用域和访问父级作用域的其他属性?”--使用 scope: true。您的指令将获得自己的作用域,该作用域从父级作用域原型继承。因此,您可以定义自己的属性和/或引用父级属性。如果您需要写入父级属性,请确保它是对象属性(例如,model.prop1),而不是原始属性(prop1)。 - Mark Rajcok

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接