Как получить доступ к родительской области из пользовательской директивы * с собственной областью * в AngularJS?

Я ищу любой способ доступа к "родительской" области видимости в директиве. Любая комбинация области видимости, transclude, require, передачи переменных (или самой области видимости) сверху и т. Д. Я полностью готов отклониться назад, но я хочу избежать чего-то совершенно хакерского или не поддерживаемого. Например, я знаю, что могу сделать это прямо сейчас, взяв $scope из параметров preLink и итерации по нему $sibling Области, чтобы найти концептуального "родителя".

Что я действительно хочу, так это уметь $watch выражение в родительской области видимости. Если я могу сделать это, то я могу выполнить то, что я пытаюсь сделать здесь: AngularJS - Как сделать партиал с переменными?

Важным примечанием является то, что директива должна использоваться повторно в той же родительской области. Поэтому поведение по умолчанию (scope: false) не работает для меня. Мне нужен отдельный объем для каждого экземпляра директивы, а затем мне нужно $watch переменная, которая живет в родительской области видимости.

Пример кода стоит 1000 слов, поэтому:

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 ответов

Решение

См. Каковы нюансы области действия прототипного / прототипического наследования в AngularJS?

Подводя итог: способ, которым директива обращается к своему родителю ($parent) область действия зависит от типа области действия, создаваемой директивой:

  1. дефолт (scope: false) - директива не создает новую область видимости, поэтому здесь нет наследования. Область действия директивы такая же, как у родительского элемента / контейнера. В функции ссылки используйте первый параметр (обычно scope).

  2. scope: true - директива создает новую дочернюю область, которая прототипно наследуется от родительской области. Свойства, определенные в родительской области, доступны для директивы scope (из-за наследования прототипа). Остерегайтесь записи в примитивное свойство области действия - это создаст новое свойство в области действия директивы (которая скрывает / скрывает родительское свойство области с тем же именем).

  3. scope: { ... } - директива создает новую изолированную / изолированную область. Он не наследует прототип родительской области видимости. Вы все еще можете получить доступ к родительской области, используя $parent, но это обычно не рекомендуется. Вместо этого вы должны указать, какие родительские свойства области (и / или функцию) нужны директиве, через дополнительные атрибуты того же элемента, где используется директива, используя =, @, а также & нотации.

  4. transclude: true - директива создает новую "включенную" дочернюю область, которая прототипически наследуется от родительской области. Если директива также создает изолированную область видимости, то включенные и изолированные области являются братьями и сестрами. $parent свойство каждой области ссылается на одну и ту же родительскую область.
    Обновление Angular v1.3: если директива также создает изолированную область, то включенная область теперь является дочерней для изолированной области. Трансклюзивные и изолирующие области больше не являются родными братьями. $parent свойство включенной области теперь ссылается на область изоляции.

Приведенная выше ссылка содержит примеры и изображения всех 4 типов.

Вы не можете получить доступ к области действия в функции компиляции директивы (как упомянуто здесь: https://github.com/angular/angular.js/wiki/Understanding-Directives). Вы можете получить доступ к области действия директивы в функции ссылки.

Наблюдение:

Для 1. и 2. выше: обычно вы указываете, какое родительское свойство нужно директиве через атрибут, затем $watch it:

<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() { ... });

Для 3. выше (изолировать область видимости) следите за именем, которое вы даете свойству директивы, используя @ или же = обозначения:

<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() { ... });

Доступ к методу контроллера означает доступ к методу в родительской области действия из директивы controller / link / scope.

Если директива разделяет / наследует родительскую область видимости, тогда достаточно просто вызвать метод родительской области видимости.

Немного больше работы требуется, если вы хотите получить доступ к методу родительской области действия из области действия изолированной директивы.

Существует несколько опций (может быть больше перечисленных ниже) для вызова метода родительской области из изолированной области действия директив или просмотра переменных родительской области (специально для опции № 6).

Обратите внимание, что я использовал link function в этих примерах, но вы можете использовать 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

<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. Через литерал объекта и из директивы link / scope

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"
    }]
});

рабочий plnkr: http://plnkr.co/edit/Jo6FcYfVXCCg3vH42BIz?p=preview

Вариант № 4. Через ссылку на функцию и из директивы link / scope

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-модели и двухсторонней привязки вы можете обновить родительские переменные области видимости., Поэтому в некоторых случаях вам может не потребоваться вызывать родительские функции области видимости.

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 во всех приведенных выше примерах, если элементы изменяются в родительской области, элементы в директиве также отражают изменения.

Если вы хотите посмотреть другие атрибуты или объекты из родительской области, вы можете сделать это, используя $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

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

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 для подробного объяснения директив.

 scope: false
 transclude: false

и у вас будет такая же область (с родительским элементом)

$scope.$watch(...

Есть много способов получить доступ к родительской области видимости, в зависимости от этих двух параметров: видимость и включение.

Вот трюк, который я использовал один раз: создайте директиву "dummy" для хранения родительской области и поместите ее где-нибудь за пределами желаемой директивы. Что-то вроде:

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>

Возможно, не самое изящное решение, но оно сделало свою работу.

Если вы используете классы ES6 и ControllerAs синтаксис, вам нужно сделать что-то немного другое.

Смотрите фрагмент ниже и обратите внимание, что vm это ControllerAs значение родительского контроллера, как используется в родительском HTML

myApp.directive('name', function() {
  return {
    // no scope definition
    link : function(scope, element, attrs, ngModel) {

        scope.vm.func(...)

Перепробовав все, я наконец-то нашел решение.

Просто поместите следующее в ваш шаблон:

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

Он просто записывает родительский атрибут / переменную области, к которой вы хотите получить доступ к текущей области.

Также обратите внимание на ; '' в конце инструкции нужно убедиться, что в вашем шаблоне нет вывода. (Angular оценивает каждое утверждение, но выводит только последнее).

Это немного глупо, но после нескольких часов проб и ошибок, оно делает свою работу.

Другие вопросы по тегам