如何在AngularJS中的自定义指令的本地作用域内访问父作用域?

336
我正在寻找任何一种在指令内访问“父”作用域的方法。可以使用作用域(scope)、传递变量(或作用域本身)等方式来实现,但要避免使用完全不可维护或者非常巧妙的方法。例如,我知道我现在可以通过从preLink参数中获取$scope并迭代它的$sibling作用域来找到概念上的“父级”,但这样做不是我想要的。
我真正想要的是能够在父作用域中$watch表达式。如果我能做到这一点,我就可以完成我在这里尝试做的事情:AngularJS - How to render a partial with variables? 一个重要的注意事项是,该指令必须在同一父作用域内可重复使用。因此,默认行为(scope: false)对我不起作用。我需要每个指令实例都有一个单独的作用域,然后我需要$watch一个存在于父作用域中的变量。
代码示例胜过千言万语,所以:
app.directive('watchingMyParentScope', function() {
    return {
        require: /* ? */,
        scope: /* ? */,
        transclude: /* ? */,
        controller: /* ? */,
        compile: function(el,attr,trans) {
            // Can I get the $parent from the transclusion function somehow?
            return {
                pre: function($s, $e, $a, parentControl) {
                    // Can I get the $parent from the parent controller?
                    // By setting this.$scope = $scope from within that controller?

                    // Can I get the $parent from the current $scope?

                    // Can I pass the $parent scope in as an attribute and define
                    // it as part of this directive's scope definition?

                    // What don't I understand about how directives work and
                    // how their scope is related to their parent?
                },
                post: function($s, $e, $a, parentControl) {
                    // Has my situation improved by the time the postLink is called?
                }
            }
        }
    };
});
6个回答

657
请参见AngularJS 中的作用域原型/原型继承细节。概括地说,指令访问其父级 ($parent) 作用域的方式取决于指令创建的作用域类型:
  1. 默认 (scope: false) - 指令不会创建新的作用域,所以这里没有继承。指令的作用域与父/容器的作用域相同。在链接函数中,使用第一个参数(通常是 scope)。

  2. scope: true - 指令创建一个新的子作用域,该作用域从父作用域继承原型。在父作用域中定义的属性可在指令作用域中使用(因为原型继承)。但要小心对基元作用域属性进行写操作 -- 这将在指令作用域上创建一个新属性(隐藏/覆盖同名的父作用域属性)。

  3. scope: { ... } - 指令创建一个新的隔离/独立作用域。它不会从父作用域继承。仍然可以使用 $parent 访问父作用域,但这通常不建议这样做。相反,应通过在同一元素上使用附加属性指定指令需要哪些父作用域属性(和/或函数),并使用 =@& 符号。

  4. transclude: true - 指令创建一个新的“插入”子作用域,该作用域从父作用域继承原型。如果指令还创建了隔离作用域,则插入和隔离作用域是兄弟关系。每个作用域的 $parent 属性都引用相同的父作用域。
    Angular v1.3 更新: 如果指令还创建了隔离作用域,则插入作用域现在是隔离作用域的子级。插入和隔离作用域不再是兄弟关系。插入作用域的 $parent 属性现在引用隔离作用域。

以上链接提供了所有 4 种类型的示例和图片。 不能在指令的编译函数中访问作用域(如此处所述:https://github.com/angular/angular.js/wiki/Dev-Guide:-Understanding-Directives)。可以在链接函数中访问指令的作用域。 监视: 对于上述第 1 和 2 类:通常通过属性指定指令需要哪些父属性,然后 $watch 它。
<div my-dir attr1="prop1"></div>
scope.$watch(attrs.attr1, function() { ... });

如果您正在观察一个对象属性,您需要使用$parse:

<div my-dir attr2="obj.prop2"></div>
var model = $parse(attrs.attr2);
scope.$watch(model, function() { ... });

对于第三点(隔离作用域),请注意使用@=符号来命名指令属性:

<div my-dir attr3="{{prop3}}" attr4="obj.prop4"></div>
scope: {
  localName3: '@attr3',
  attr4:      '='  // here, using the same name as the attribute
},
link: function(scope, element, attrs) {
   scope.$watch('localName3', function() { ... });
   scope.$watch('attr4',      function() { ... });

1
谢谢,Mark。事实证明我在如何使用变量呈现部分上发布的解决方案确实非常漂亮。你真正需要链接给我的是一个标题为“编写HTML的微妙之处,并认识到你认为它不在ng-controller中嵌套”的东西。哇...新手错误。但这是对你其他(更长)解释作用域的有用补充。 - colllin
1
@Andy,不要在=上使用$parsefiddle$parse仅适用于非隔离范围。 - Mark Rajcok
1
这是一个非常全面的好答案。它还阐明了我为什么简直讨厌使用AngularJS的原因。 - John Trichereau
谢谢 - 点1到4是我找到的最简单的解释。 - br3w5
我发现一个情况,似乎需要使用$scope.$parent - 当我想将事件处理程序(例如ng-focus)从我的自定义指令重定向到指令内部的元素时。如果我只是使用= 进行绑定,那么它会立即被Angular评估,但我需要在我的指令中的事件发生时稍后再进行评估。所以我可以在指令的ng-focus处理程序中这样做:directiveSscope.$parent.$eval($scope.ngFocusExpression); - JustAMartin
显示剩余7条评论

54

访问控制器方法意味着从指令控制器/链接/作用域中访问父级作用域上的方法。

如果指令共享或继承了父作用域,则很容易调用父作用域方法。

当您想要从隔离的指令作用域访问父作用域方法时,需要进行更多的工作。

有几个选项(可能不止下面列出的选项)可以从隔离的指令作用域中调用父作用域方法或监视父作用域变量(特别是选项#6)。

注意,这些示例中我使用了link函数,但根据需要也可以使用directive controller

选项#1。通过对象文字和指令HTML模板传递

index.html

<!DOCTYPE html>
<html ng-app="plunker">

  <head>
    <meta charset="utf-8" />
    <title>AngularJS Plunker</title>
    <script>document.write('<base href="' + document.location + '" />');</script>
    <link rel="stylesheet" href="style.css" />
    <script data-require="angular.js@1.3.x" src="https://code.angularjs.org/1.3.9/angular.js" data-semver="1.3.9"></script>
    <script src="app.js"></script>
  </head>

  <body ng-controller="MainCtrl">
    <p>Hello {{name}}!</p>

    <p> Directive Content</p>
    <sd-items-filter selected-items="selectedItems" selected-items-changed="selectedItemsChanged(selectedItems)" items="items"> </sd-items-filter>


    <P style="color:red">Selected Items (in parent controller) set to: {{selectedItemsReturnedFromDirective}} </p>

  </body>

</html>

itemfilterTemplate.html

:这是表示文件名为“itemfilterTemplate.html”的代码片段。
<select ng-model="selectedItems" multiple="multiple" style="height: 200px; width: 250px;" ng-change="selectedItemsChanged({selectedItems:selectedItems})" ng-options="item.id as item.name group by item.model for item in items | orderBy:'name'">
  <option>--</option>
</select>
app.js
var app = angular.module('plunker', []);

app.directive('sdItemsFilter', function() {
  return {
    restrict: 'E',
    scope: {
      items: '=',
      selectedItems: '=',
      selectedItemsChanged: '&'
    },
    templateUrl: "itemfilterTemplate.html"
  }
})

app.controller('MainCtrl', function($scope) {
  $scope.name = 'TARS';

  $scope.selectedItems = ["allItems"];

  $scope.selectedItemsChanged = function(selectedItems1) {
    $scope.selectedItemsReturnedFromDirective = selectedItems1;
  }

  $scope.items = [{
    "id": "allItems",
    "name": "All Items",
    "order": 0
  }, {
    "id": "CaseItem",
    "name": "Case Item",
    "model": "PredefinedModel"
  }, {
    "id": "Application",
    "name": "Application",
    "model": "Bank"
    }]

});

工作中的plnkr: http://plnkr.co/edit/rgKUsYGDo9O3tewL6xgr?p=preview

选项#2. 通过对象文字和指令链接/范围

index.html

<!DOCTYPE html>
<html ng-app="plunker">

  <head>
    <meta charset="utf-8" />
    <title>AngularJS Plunker</title>
    <script>document.write('<base href="' + document.location + '" />');</script>
    <link rel="stylesheet" href="style.css" />
    <script data-require="angular.js@1.3.x" src="https://code.angularjs.org/1.3.9/angular.js" data-semver="1.3.9"></script>
    <script src="app.js"></script>
  </head>

  <body ng-controller="MainCtrl">
    <p>Hello {{name}}!</p>

    <p> Directive Content</p>
    <sd-items-filter selected-items="selectedItems" selected-items-changed="selectedItemsChanged(selectedItems)" items="items"> </sd-items-filter>


    <P style="color:red">Selected Items (in parent controller) set to: {{selectedItemsReturnedFromDirective}} </p>

  </body>

</html>

itemfilterTemplate.html

<select ng-model="selectedItems" multiple="multiple" style="height: 200px; width: 250px;" 
 ng-change="selectedItemsChangedDir()" ng-options="item.id as item.name group by item.model for item in items | orderBy:'name'">
  <option>--</option>
</select>

app.js

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

app.directive('sdItemsFilter', function() {
  return {
    restrict: 'E',
    scope: {
      items: '=',
      selectedItems: '=',
      selectedItemsChanged: '&'
    },
    templateUrl: "itemfilterTemplate.html",
    link: function (scope, element, attrs){
      scope.selectedItemsChangedDir = function(){
        scope.selectedItemsChanged({selectedItems:scope.selectedItems});  
      }
    }
  }
})

app.controller('MainCtrl', function($scope) {
  $scope.name = 'TARS';

  $scope.selectedItems = ["allItems"];

  $scope.selectedItemsChanged = function(selectedItems1) {
    $scope.selectedItemsReturnedFromDirective = selectedItems1;
  }

  $scope.items = [{
    "id": "allItems",
    "name": "All Items",
    "order": 0
  }, {
    "id": "CaseItem",
    "name": "Case Item",
    "model": "PredefinedModel"
  }, {
    "id": "Application",
    "name": "Application",
    "model": "Bank"
    }]
});

工作中的 plnkr:http://plnkr.co/edit/BRvYm2SpSpBK9uxNIcTa?p=preview

选项 #3. 通过函数引用和指令 HTML 模板

index.html

<!DOCTYPE html>
<html ng-app="plunker">

  <head>
    <meta charset="utf-8" />
    <title>AngularJS Plunker</title>
    <script>document.write('<base href="' + document.location + '" />');</script>
    <link rel="stylesheet" href="style.css" />
    <script data-require="angular.js@1.3.x" src="https://code.angularjs.org/1.3.9/angular.js" data-semver="1.3.9"></script>
    <script src="app.js"></script>
  </head>

  <body ng-controller="MainCtrl">
    <p>Hello {{name}}!</p>

    <p> Directive Content</p>
    <sd-items-filter selected-items="selectedItems" selected-items-changed="selectedItemsChanged" items="items"> </sd-items-filter>


    <P style="color:red">Selected Items (in parent controller) set to: {{selectedItemsReturnFromDirective}} </p>

  </body>

</html>
itemfilterTemplate.html
<select ng-model="selectedItems" multiple="multiple" style="height: 200px; width: 250px;" 
 ng-change="selectedItemsChanged()(selectedItems)" ng-options="item.id as item.name group by item.model for item in items | orderBy:'name'">
  <option>--</option>
</select>

app.js

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

app.directive('sdItemsFilter', function() {
  return {
    restrict: 'E',
    scope: {
      items: '=',
      selectedItems:'=',
      selectedItemsChanged: '&'
    },
    templateUrl: "itemfilterTemplate.html"
  }
})

app.controller('MainCtrl', function($scope) {
  $scope.name = 'TARS';

  $scope.selectedItems = ["allItems"];

  $scope.selectedItemsChanged = function(selectedItems1) {
    $scope.selectedItemsReturnFromDirective = selectedItems1;
  }

  $scope.items = [{
    "id": "allItems",
    "name": "All Items",
    "order": 0
  }, {
    "id": "CaseItem",
    "name": "Case Item",
    "model": "PredefinedModel"
  }, {
    "id": "Application",
    "name": "Application",
    "model": "Bank"
    }]
});

working plnkr: http://plnkr.co/edit/Jo6FcYfVXCCg3vH42BIz?p=preview

选项#4. 通过函数引用和指令链接/作用域来实现

index.html

<!DOCTYPE html>
<html ng-app="plunker">

  <head>
    <meta charset="utf-8" />
    <title>AngularJS Plunker</title>
    <script>document.write('<base href="' + document.location + '" />');</script>
    <link rel="stylesheet" href="style.css" />
    <script data-require="angular.js@1.3.x" src="https://code.angularjs.org/1.3.9/angular.js" data-semver="1.3.9"></script>
    <script src="app.js"></script>
  </head>

  <body ng-controller="MainCtrl">
    <p>Hello {{name}}!</p>

    <p> Directive Content</p>
    <sd-items-filter selected-items="selectedItems" selected-items-changed="selectedItemsChanged" items="items"> </sd-items-filter>


    <P style="color:red">Selected Items (in parent controller) set to: {{selectedItemsReturnedFromDirective}} </p>

  </body>

</html>
itemfilterTemplate.html
<select ng-model="selectedItems" multiple="multiple" style="height: 200px; width: 250px;" ng-change="selectedItemsChangedDir()" ng-options="item.id as item.name group by item.model for item in items | orderBy:'name'">
  <option>--</option>
</select>

app.js

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

app.directive('sdItemsFilter', function() {
  return {
    restrict: 'E',
    scope: {
      items: '=',
      selectedItems: '=',
      selectedItemsChanged: '&'
    },
    templateUrl: "itemfilterTemplate.html",
    link: function (scope, element, attrs){
      scope.selectedItemsChangedDir = function(){
        scope.selectedItemsChanged()(scope.selectedItems);  
      }
    }
  }
})

app.controller('MainCtrl', function($scope) {
  $scope.name = 'TARS';

  $scope.selectedItems = ["allItems"];

  $scope.selectedItemsChanged = function(selectedItems1) {
    $scope.selectedItemsReturnedFromDirective = selectedItems1;
  }

  $scope.items = [{
    "id": "allItems",
    "name": "All Items",
    "order": 0
  }, {
    "id": "CaseItem",
    "name": "Case Item",
    "model": "PredefinedModel"
  }, {
    "id": "Application",
    "name": "Application",
    "model": "Bank"
    }]

});

正在使用的Plnkr:http://plnkr.co/edit/BSqx2J1yCY86IJwAnQF1?p=preview

选项#5:通过ng-model和双向绑定,您可以更新父级作用域变量。因此,在某些情况下,您可能无需调用父级作用域函数。

index.html

<!DOCTYPE html>
<html ng-app="plunker">

  <head>
    <meta charset="utf-8" />
    <title>AngularJS Plunker</title>
    <script>document.write('<base href="' + document.location + '" />');</script>
    <link rel="stylesheet" href="style.css" />
    <script data-require="angular.js@1.3.x" src="https://code.angularjs.org/1.3.9/angular.js" data-semver="1.3.9"></script>
    <script src="app.js"></script>
  </head>

  <body ng-controller="MainCtrl">
    <p>Hello {{name}}!</p>

    <p> Directive Content</p>
    <sd-items-filter ng-model="selectedItems" selected-items-changed="selectedItemsChanged" items="items"> </sd-items-filter>


    <P style="color:red">Selected Items (in parent controller) set to: {{selectedItems}} </p>

  </body>

</html>
itemfilterTemplate.html
<select ng-model="selectedItems" multiple="multiple" style="height: 200px; width: 250px;" 
 ng-options="item.id as item.name group by item.model for item in items | orderBy:'name'">
  <option>--</option>
</select>

app.js

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

app.directive('sdItemsFilter', function() {
  return {
    restrict: 'E',
    scope: {
      items: '=',
      selectedItems: '=ngModel'
    },
    templateUrl: "itemfilterTemplate.html"
  }
})

app.controller('MainCtrl', function($scope) {
  $scope.name = 'TARS';

  $scope.selectedItems = ["allItems"];

  $scope.items = [{
    "id": "allItems",
    "name": "All Items",
    "order": 0
  }, {
    "id": "CaseItem",
    "name": "Case Item",
    "model": "PredefinedModel"
  }, {
    "id": "Application",
    "name": "Application",
    "model": "Bank"
    }]
});

工作中的plnkr:http://plnkr.co/edit/hNui3xgzdTnfcdzljihY?p=preview

选项#6:通过$watch$watchCollection 以上所有示例中的items都是双向绑定的,如果在父作用域中修改了items,则指令中的items也会反映出更改。

如果要监视来自父作用域的其他属性或对象,则可以使用$watch$watchCollection。如下所示:

HTML

<!DOCTYPE html>
<html ng-app="plunker">

<head>
  <meta charset="utf-8" />
  <title>AngularJS Plunker</title>
  <script>
    document.write('<base href="' + document.location + '" />');
  </script>
  <link rel="stylesheet" href="style.css" />
  <script data-require="angular.js@1.3.x" src="https://code.angularjs.org/1.3.9/angular.js" data-semver="1.3.9"></script>
  <script src="app.js"></script>
</head>

<body ng-controller="MainCtrl">
  <p>Hello {{user}}!</p>
  <p>directive is watching name and current item</p>
  <table>
    <tr>
      <td>Id:</td>
      <td>
        <input type="text" ng-model="id" />
      </td>
    </tr>
    <tr>
      <td>Name:</td>
      <td>
        <input type="text" ng-model="name" />
      </td>
    </tr>
    <tr>
      <td>Model:</td>
      <td>
        <input type="text" ng-model="model" />
      </td>
    </tr>
  </table>

  <button style="margin-left:50px" type="buttun" ng-click="addItem()">Add Item</button>

  <p>Directive Contents</p>
  <sd-items-filter ng-model="selectedItems" current-item="currentItem" name="{{name}}" selected-items-changed="selectedItemsChanged" items="items"></sd-items-filter>

  <P style="color:red">Selected Items (in parent controller) set to: {{selectedItems}}</p>
</body>

</html>

脚本 app.js

定义了一个名为“plunker”的Angular模块。

app.directive('sdItemsFilter', function() {
  return {
    restrict: 'E',
    scope: {
      name: '@',
      currentItem: '=',
      items: '=',
      selectedItems: '=ngModel'
    },
    template: '<select ng-model="selectedItems" multiple="multiple" style="height: 140px; width: 250px;"' +
      'ng-options="item.id as item.name group by item.model for item in items | orderBy:\'name\'">' +
      '<option>--</option> </select>',
    link: function(scope, element, attrs) {
      scope.$watchCollection('currentItem', function() {
        console.log(JSON.stringify(scope.currentItem));
      });
      scope.$watch('name', function() {
        console.log(JSON.stringify(scope.name));
      });
    }
  }
})

 app.controller('MainCtrl', function($scope) {
  $scope.user = 'World';

  $scope.addItem = function() {
    $scope.items.push({
      id: $scope.id,
      name: $scope.name,
      model: $scope.model
    });
    $scope.currentItem = {};
    $scope.currentItem.id = $scope.id;
    $scope.currentItem.name = $scope.name;
    $scope.currentItem.model = $scope.model;
  }

  $scope.selectedItems = ["allItems"];

  $scope.items = [{
    "id": "allItems",
    "name": "All Items",
    "order": 0
  }, {
    "id": "CaseItem",
    "name": "Case Item",
    "model": "PredefinedModel"
  }, {
    "id": "Application",
    "name": "Application",
    "model": "Bank"
  }]
});

您可以参考 AngularJS 文档获取关于指令的详细解释。


10
他为了他的声誉努力工作……非常为了他的声誉努力工作……他为了自己的声誉而非常努力工作,所以你最好给他点赞。 - slim
7
被踩 - 回答中有价值的信息因为过长而无法获取。 - redress
2
我用清晰的分隔回答了所有可用的替代方案。在我看来,除非你面前有一个大局观,否则简短的答案并不总是有帮助的。 - Yogesh Manware
@YogeshManware: 如果省略与样式表无关的内容,不使用冗长的标记语言,简化示例以不使用"分组 by"等内容,则可以大大缩短它。对于每个示例进行说明也非常有用。 - damd
这不是一个下投票的理由。人们滥用了这个特权。 - Winnemucca
#6 就是我要找的,谢谢。 - balron

11
 scope: false
 transclude: false

你将拥有与父元素相同的范围

$scope.$watch(...

根据作用域(scope)和引用传递(transclude)这两个选项,有很多方法可以访问父级作用域。


是的,简短明了且正确。它们似乎与父元素具有完全相同的作用域...这使得它们在相同的作用域中无法重复使用。http://jsfiddle.net/collindo/xqytH/ - colllin
2
很多时候,当我们编写可重用组件时,需要使用隔离作用域,因此解决方案并不那么简单。 - Yvon Huynh

8

这里有一个我曾经使用过的技巧:创建一个“虚拟”指令来存储父级作用域,并将其放置在所需指令之外的某个位置。例如:

module.directive('myDirectiveContainer', function () {
    return {
        controller: function ($scope) {
            this.scope = $scope;
        }
    };
});

module.directive('myDirective', function () {
    return {
        require: '^myDirectiveContainer',
        link: function (scope, element, attrs, containerController) {
            // use containerController.scope here...
        }
    };
});

然后

<div my-directive-container="">
    <div my-directive="">
    </div>
</div>

也许这不是最优美的解决方案,但它已经完成了工作。

4
如果您正在使用ES6类和ControllerAs语法,则需要稍微做些不同的事情。
请参阅下面的片段,并注意vm是父控制器中用于父HTML的ControllerAs值。
myApp.directive('name', function() {
  return {
    // no scope definition
    link : function(scope, element, attrs, ngModel) {

        scope.vm.func(...)

-1

尝试了一切之后,我终于想出了一个解决方案。

只需在您的模板中放置以下内容:

{{currentDirective.attr = parentDirective.attr; ''}}

它只是将您想要访问的父级作用域属性/变量写入当前作用域。

还请注意语句末尾的;'',这是为了确保模板中没有输出。(Angular会评估每个语句,但仅输出最后一个)。

这有点hacky,但经过几个小时的试错,它可以完成工作。


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