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

327

Я ищу любой способ доступа к "родительской" области видимости в директиве. Любая комбинация области видимости, 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?
                }
            }
        }
    };
});
colllin
источник

Ответы:

644

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

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

  1. default ( 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:

<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() { ... });
Марк Райкок
источник
1
СПАСИБО, Марк. Оказывается, решение, которое я разместил на том, как визуализировать партиал с переменными, действительно работает довольно красиво. Что вам действительно нужно было связать со мной, так это что-то под названием «Нюансы написания HTML и признание того, что ваш элемент не вложен в контроллер ng, как вы думаете». Ух ты ... ошибка новичка. Но это полезное дополнение к вашему другому (гораздо более длинному) ответу с объяснением границ.
Коллин
@collin, отлично, я рад, что вы решили свою проблему, так как я не совсем знал, как ответить на ваш другой (теперь удаленный) комментарий.
Марк Райкок
Какие вещи я могу / должен исполнять внутриscope.$watch('localName3', function() { ...[?? WHAT TO DO HERE for example?] });
Джунаид Кадир
1
@ Энди, нет, не используйте $parseс =: скрипкой . $parseнужен только для неизолированных областей.
Марк Райкок
1
Это отличный ответ, очень тщательный. Это также показывает, почему я просто ненавижу работать с AngularJS.
Джон Тришеро
51

Доступ к методу контроллера означает доступ к методу родительской области из директивы 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 для подробного объяснения директив.

Йогеш Manware
источник
10
Он усердно работает для своего представителя ... так усердно для своего представителя ... он усердно работает для своего представителя, так что вам лучше сказать его правильно.
похудеть
7
недооценено - любая ценная информация в ответе недоступна из-за его длины
исправление
2
Я ответил на вопрос со всеми доступными альтернативами с четким разделением. На мой взгляд, короткие ответы не всегда полезны, пока перед вами не появится большая картина.
Йогеш Мэнвар
@YogeshManware: его можно было бы значительно сократить, исключив ненужные вещи, такие как таблицы стилей, не используя длинную разметку, упростив примеры, чтобы не использовать такие вещи, как «сгруппировать по» и т. Д. Это также было бы очень полезно с некоторыми пояснениями для каждый пример.
Черт возьми
Это не повод для отрицательного голосования. Люди злоупотребляют этой привилегией
Winnemucca
11
 scope: false
 transclude: false

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

$scope.$watch(...

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

Степан Суворов
источник
Да, коротко и мило, и правильно. Похоже, они имеют ту же область видимости, что и родительский элемент ... что делает невозможным их повторное использование в той же области видимости. jsfiddle.net/collindo/xqytH
коллин
2
много раз нам нужна изолированная область видимости, когда мы пишем компонент многократного использования, поэтому решение не такое простое
Ивон Хьюн
8

Вот трюк, который я использовал один раз: создайте директиву «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>

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

anewcomer
источник
4

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

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

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

        scope.vm.func(...)
Саймон Х
источник
0

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

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

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

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

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

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

Джеффри Розендал
источник