从指令访问控制器作用域

21

我创建了一个简单的指令,用于显示我正在创建的<table>的排序列标题。

ngGrid.directive("sortColumn", function() {
    return {
        restrict: "E",
        replace: true,
        transclude: true,
        scope: {
            sortby: "@",
            onsort: "="
        },
        template: "<span><a href='#' ng-click='sort()' ng-transclude></a></span>",
        link: function(scope, element, attrs) {
            scope.sort = function () {

                // I want to call CONTROLLER.onSort here, but how do I access the controller scope?...
                scope.controllerOnSort(scope.sortby);
            };
        }
    };
});

这是一个创建表头的例子:

<table id="mainGrid" ng-controller="GridCtrl>
<thead>
    <tr>
        <th><sort-column sortby="Name">Name</sort-column></th>
        <th><sort-column sortby="DateCreated">Date Created</sort-column></th>
        <th>Hi</th>
    </tr>
</thead>

当单击排序列时,我希望在我的网格控制器上触发onControllerSort函数..但是我卡住了!到目前为止,我能够做到的唯一方法是为每个<sort-column>添加“ onSort”属性,并在指令中引用这些属性:

因此,当点击排序列时,我希望能够在我的网格控制器上触发onControllerSort函数。但是我遇到了困难!目前为止,我唯一能够做的就是为每个<sort-column>添加"onSort"属性,并在指令中引用这些属性:

<sort-column onSort="controllerOnSort" sortby="Name">Name</sort-column>

但这并不好,因为我总是想调用 controllerOnSort,所以在每个指令中都添加它有点丑陋。有没有办法在不需要在HTML中添加不必要的标记的情况下,在指令内部实现这个功能?如果可以的话,该指令和控制器都在同一个模块中定义。

5个回答

24

创建第二个指令作为包装器:

ngGrid.directive("columnwrapper", function() {
  return {
    restrict: "E",
    scope: {
      onsort: '='
    }
  };
});

那么您只需要在外部指令中引用该函数以便调用:

<columnwrapper onsort="controllerOnSort">
  <sort-column sortby="Name">Name</sort-column>
  <sort-column sortby="DateCreated">Date Created</sort-column>
</columnwrapper>
在 "sortColumn" 指令中,您可以通过调用引用的函数来调用它。
scope.$parent.onsort();

看这个示例进行实际操作:http://jsfiddle.net/wZrjQ/1/

当然,如果你不介意硬编码依赖项,你也可以只使用一个指令,并通过父作用域(即相关的控制器)调用该函数。

scope.$parent.controllerOnSort():

我有另一个演示这个问题的代码片段: http://jsfiddle.net/wZrjQ/2

这个解决方案与其他答案中的解决方案(https://dev59.com/42Ik5IYBdhLWcg3wUsrv#19385937) 产生了相同的影响(关于硬耦合的批评是一样的),但比那个解决方案稍微容易些。如果你无论如何都要硬耦合,我认为没有必要将控制器引用作为点,因为它很可能一直在$scope.$parent上可用(但要注意其他元素设置作用域)。

然而,我会选择第一种解决方案。它增加了一些小的标记,但解决了问题并保持了干净的分离。此外,如果您使用第二个指令作为直接包装器,则可以确保$scope.$parent匹配外部指令。


我更喜欢这个方案,因为它比另一个方案更简单。尽管在这种情况下我仍然认为紧密耦合对我并不重要,因为我正在制作一个网格控件,而指令仅用于网格控件... - Matt Roberts
我尝试按照您的建议1创建一个父指令,但是我好像也遇到了问题 - 我不确定如何访问父指令的“作用域”.. 我在这里发布了另一个带有plunker的问题:http://stackoverflow.com/questions/19405176/table-headers-with-sort-indicators - Matt Roberts
2
虽然在这里它能够工作,但使用 $parent 是一种不好的模式。它只能在更深层次上工作一次,因此很难重复使用。 - Carlos Morales
使用具有 '=' 的双向绑定已经过时,并且会使迁移到 Angular 2+ 更加困难。有关更多信息,请参见 AngularJS 开发人员指南 - 基于组件的应用程序架构 - georgeawg

20

&本地作用域属性允许指令的使用者传递一个函数,该函数可以被指令调用。

&作用域属性图示

详见此处

这里有一个类似问题的答案,展示了如何从指令代码中将参数传递给回调函数。


8
在您的指令中需要引入ngController,并将链接函数修改为:
ngGrid.directive("sortColumn", function() {
    return {
        ...
        require: "ngController",
        ...
        link: function(scope, element, attrs, ngCtrl) {
            ...
        }
    };
});

你得到的是你的控制器GridCtrl,作为ngCtrl。但你并没有获取它的作用域,你需要在以下代码中进行操作:
xxxx.controller("GridCtrl", function($scope, ...) {
    // add stuff to scope as usual
    $scope.xxxx = yyyy;

    // Define controller public API
    // NOTE: USING this NOT $scope
    this.controllerOnSort = function(...) { ... };
});

在链接函数中简单地调用它:

ngCtrl.controllerOnSort(...);

请注意,此需求将获取第一个父级ngController。 如果在GridCtrl和指令之间指定了另一个控制器,则会获取该控制器。
演示这个原理的fiddle(一个指令访问具有方法的父ng-controller):http://jsfiddle.net/NAfm5/1/
人们担心这种解决方案可能会引入不必要的紧密耦合。 如果确实存在这样的问题,则可以解决如下:
创建一个将与控制器并排的指令,称为master:
<table id="mainGrid" ng-controller="GridCtrl" master="controllerOnSort()">

这个指令涉及控制器的所需方法(因此:解耦)。
子指令(在您的情况下为“sort-column”)需要主指令。
require: "^master"

使用$parse服务,可以从主控制器的成员方法中调用指定的方法。请参见实现此原则的更新后的fiddle:http://jsfiddle.net/NAfm5/3/


我不确定这种紧密耦合指令和控制器的方式是否是最佳选择。它肯定可以工作,但每次使用此指令时都需要在控制器上定义一个非常特定的方法。目前他所做的方式可以说是更好的实践。 - Adam
事实上,这些组件已经耦合在一起,因为其中一个需要另一个进行正确的操作。该指令依赖于控制器(任何控制器)存在,该控制器具有特定方法,在此处为 controllerOnSort()。这等同于在强类型语言中实现控制器接口。父级控制器可以使用 require: "?^ngController" 参数设置为可选。 - Nikos Paraskevopoulos
不是真的,因为如果你看他的问题,他是通过属性传递回调函数的。 <sort-column onSort="controllerOnSort" sortby="Name">Name</sort-column> 这将指令与任何控制器或作用域解耦。在他的示例中,排序方法可以命名为任何名称,而你的则需要严格命名。他的示例还允许从层次结构中的任何作用域传递方法,而你的则寻找直接父级。他的抱怨只是他必须在所有列中重复属性。 - Adam
的确,这是一种权衡之后的方式:你获得了方便,但失去了耦合(理论上,因为这些事情已经在逻辑上相互耦合)。 - Nikos Paraskevopoulos
感谢您的详细回复。"紧耦合"是一个有效的观点,但对于这个用途,我并不介意,因为我只想让指令与网格控件一起使用,所以在这种情况下,我认为它的紧密耦合并不是一个大问题。 - Matt Roberts

2

虽然由于我的相对缺乏经验,我不能保证这种解决方案的适用性,但还有另一种方法可以实现这个功能。无论如何,我会将其传递给您,供您参考。

在您的列中,您可以创建一个作用域变量属性:

<sort-column data-sortby="sortby">Date Created</sort-column>

然后在您的控制器中定义作用域变量:

$scope.sortby = 'DateCreated' // just a default sort here

然后在控制器中添加您的排序函数:

$scope.onSort = function(val) {
    $scope.sortby = val;
}

然后在您的标记中连接ng-click:

<sort-column data-sortby="sortby" ng-click="onSort('DateCreated')">Date Created</sort-column>

然后在您的指令中,将sortby属性添加到指令作用域中:
scope: {
    sortby: '=' // not sure if you need
}

在您的“link:”函数中添加$watch:
在您的“link:”函数中,添加一个$watch:
scope.$watch('sortby', function () {
    ... your sort logic here ...
}

这种方法的优点在于指令完全解耦,您不需要从指令中回调onSort,因为在执行路径的那一部分中,您实际上没有离开控制器的onSort函数。
如果您需要告诉控制器等待排序完成,您可以在控制器中定义一个事件:
$scope.$on("_sortFinished", function(event, message){
   ..do something...  
});

然后在你的指令中简单地发出事件,那么过程就完成了:

$scope.$emit('_sortFinished');

有其他的方法可以实现这个,但这种方式会增加一些紧密的耦合,因为你的控制器必须监听特定事件,而指令必须发出特定事件......但由于它们已经密切相关,所以这可能不是问题。


1

可能听起来有点疯狂,但是通过内置的方法从元素获取控制器似乎比调整require更容易:

var mod = angular.module('something', []).directive('myDir', 
  function () {
    return {
      link: function (scope, element) {
        console.log(element.controller('myDir'));
      },
      controller: function () {
        this.works = function () {};
      },
      scope: {}
    }
  }
);

http://plnkr.co/edit/gY4rP0?p=preview


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