Comment accéder à la portée parent à partir d'une directive personnalisée * avec sa propre portée * dans AngularJS?

327

Je cherche n'importe quelle manière d'accéder à la portée "parent" dans une directive. Toute combinaison de portée, de transclude, d'exigence, de transmission de variables (ou de la portée elle-même) par le haut, etc. Je suis tout à fait disposé à se pencher en arrière, mais je veux éviter quelque chose de totalement hacky ou irréalisable. Par exemple, je sais que je pourrais le faire dès maintenant en prenant les $scopeparamètres preLink et en itérant sur ses $siblingétendues pour trouver le "parent" conceptuel.

Ce que je veux vraiment, c'est pouvoir $watchune expression dans la portée parent. Si je peux le faire, alors je peux accomplir ce que j'essaie de faire ici: AngularJS - Comment rendre un partiel avec des variables?

Il est important de noter que la directive doit être réutilisable dans le même domaine parent. Par conséquent, le comportement par défaut (portée: faux) ne fonctionne pas pour moi. J'ai besoin d'une portée individuelle par instance de la directive, puis j'ai besoin d' $watchune variable qui vit dans la portée parent.

Un exemple de code vaut 1000 mots, donc:

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
la source

Réponses:

644

Voir Quelles sont les nuances de l'héritage prototypique / prototypique de portée dans AngularJS?

Pour résumer: la façon dont une directive accède à sa $parentportée parent ( ) dépend du type de portée créée par la directive:

  1. default ( scope: false) - la directive ne crée pas de nouvelle portée, il n'y a donc pas d'héritage ici. La portée de la directive est la même portée que le parent / conteneur. Dans la fonction de liaison, utilisez le premier paramètre (généralement scope).

  2. scope: true- la directive crée une nouvelle portée enfant qui hérite de manière prototypique de la portée parent. Les propriétés définies sur la portée parent sont disponibles pour la directive scope(en raison de l'héritage prototypique). Méfiez-vous simplement d'écrire dans une propriété de portée primitive - cela créera une nouvelle propriété sur la portée de la directive (qui masque / masque la propriété de portée parent du même nom).

  3. scope: { ... }- la directive crée un nouveau champ d'isolement / d'isolement. Il n'hérite pas de manière prototype de la portée parent. Vous pouvez toujours accéder à la portée parent à l'aide de $parent, mais ce n'est généralement pas recommandé. Au lieu de cela, vous devez spécifier les propriétés champ parent (et / ou fonction) les besoins directive par des attributs supplémentaires sur le même élément dans lequel la directive est utilisée, en utilisant la =, @et la &notation.

  4. transclude: true- la directive crée une nouvelle portée enfant "transclue", qui hérite de manière prototypique de la portée parent. Si la directive crée également une étendue isolée, les étendues transclues et isolées sont des frères et sœurs. La $parentpropriété de chaque étendue fait référence à la même étendue parent.
    Mise à jour angulaire v1.3 : si la directive crée également une portée isolée, la portée transclue est désormais une enfant de la portée isolée. Les portées transclues et isolées ne sont plus des frères et sœurs. La $parentpropriété de la portée transclue fait désormais référence à la portée isolée.

Le lien ci-dessus contient des exemples et des images des 4 types.

Vous ne pouvez pas accéder à la portée dans la fonction de compilation de la directive (comme mentionné ici: https://github.com/angular/angular.js/wiki/Understanding-Directives ). Vous pouvez accéder à la portée de la directive dans la fonction de lien.

En train de regarder:

Pour 1. et 2. ci-dessus: normalement, vous spécifiez de quelle propriété parent la directive a besoin via un attribut, puis $ regardez-la:

<div my-dir attr1="prop1"></div>

scope.$watch(attrs.attr1, function() { ... });

Si vous regardez une propriété d'objet, vous devrez utiliser $ parse:

<div my-dir attr2="obj.prop2"></div>

var model = $parse(attrs.attr2);
scope.$watch(model, function() { ... });

Pour 3. ci-dessus (isoler la portée), regardez le nom que vous donnez à la propriété directive en utilisant la notation @ou =:

<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() { ... });
Mark Rajcok
la source
1
MERCI, Mark. Il s'avère que la solution que j'ai publiée sur Comment rendre un partiel avec des variables fonctionne vraiment très bien. Ce dont vous aviez vraiment besoin pour me lier était quelque chose intitulé "Les nuances de l'écriture HTML et de la reconnaissance que votre élément n'est pas imbriqué dans le contrôleur ng que vous pensez qu'il est." Wow ... erreur de débutant. Mais c'est un ajout utile à votre autre réponse (beaucoup plus longue) expliquant les portées.
colllin
@collin, super, je suis content que vous ayez résolu votre problème, car je ne savais pas trop comment répondre à votre autre commentaire (maintenant supprimé).
Mark Rajcok
Quelles sont les choses que je peux / dois-je jouer dansscope.$watch('localName3', function() { ...[?? WHAT TO DO HERE for example?] });
Junaid Qadir
1
@Andy, non ne pas utiliser $parseavec =: violon . $parsen'est nécessaire qu'avec les étendues non isolées.
Mark Rajcok
1
Ceci est une excellente réponse, très approfondie. Cela illustre également pourquoi je déteste simplement travailler avec AngularJS.
John Trichereau
51

Accéder à la méthode du contrôleur signifie accéder à une méthode sur la portée parent à partir de la directive contrôleur / lien / portée.

Si la directive partage / hérite de la portée parent, il est assez simple d'appeler simplement une méthode de portée parent.

Un peu plus de travail est requis lorsque vous souhaitez accéder à la méthode de portée parent à partir de la portée de directive isolée.

Il y a peu d'options (peut-être plus que celles listées ci-dessous) pour invoquer une méthode de portée parent à partir de la portée des directives isolées ou regarder les variables de portée parent ( option # 6 spécialement).

Notez que j'ai utilisé link functiondans ces exemples, mais vous pouvez également utiliser un directive controlleren fonction des besoins.

Option 1. Par le biais du littéral objet et du modèle de directive 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="[email protected]" 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 de travail: http://plnkr.co/edit/rgKUsYGDo9O3tewL6xgr?p=preview

Option 2. Par le biais d'un littéral d'objet et d'un lien / portée de directive

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="[email protected]" 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 de travail: http://plnkr.co/edit/BRvYm2SpSpBK9uxNIcTa?p=preview

Option n ° 3. Par référence à la fonction et à partir du modèle de directive 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="[email protected]" 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 de travail: http://plnkr.co/edit/Jo6FcYfVXCCg3vH42BIz?p=preview

Option n ° 4. Par référence à la fonction et par lien / champ d'application de la directive

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="[email protected]" 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 de travail: http://plnkr.co/edit/BSqx2J1yCY86IJwAnQF1?p=preview

Option n ° 5: via ng-model et la liaison bidirectionnelle, vous pouvez mettre à jour les variables de portée parent. . Par conséquent, vous n'aurez peut-être pas besoin d'appeler les fonctions de portée parent dans certains cas.

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="[email protected]" 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 de travail: http://plnkr.co/edit/hNui3xgzdTnfcdzljihY?p=preview

Option # 6: Through $watchet$watchCollection c'est une liaison bidirectionnelle pour itemsdans tous les exemples ci-dessus, si les éléments sont modifiés dans la portée parent, les éléments dans la directive refléteraient également les changements.

Si vous souhaitez regarder d'autres attributs ou objets de la portée parent, vous pouvez le faire en utilisant $watchet $watchCollectioncomme indiqué ci-dessous

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="[email protected]" 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>

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

Vous pouvez toujours consulter la documentation AngularJs pour des explications détaillées sur les directives.

Yogesh Manware
la source
10
Il travaille dur pour son représentant ... si dur pour son représentant ... il travaille dur pour son représentant afin que vous fassiez mieux de lui donner raison.
slim
7
downvoted - toute information précieuse dans la réponse est inaccessible en raison de sa longueur
réparation
2
J'ai répondu à la question avec toutes les alternatives disponibles avec une séparation claire. À mon avis, les réponses courtes ne sont pas toujours utiles tant que vous n'avez pas une vue d'ensemble devant vous.
Yogesh Manware
@YogeshManware: Il pourrait être beaucoup raccourci en omettant les choses non pertinentes comme les feuilles de style, en n'utilisant pas de balisage long, en simplifiant les exemples pour ne pas utiliser des choses comme "grouper par", etc. Il serait également très utile avec une sorte d'explication pour chaque exemple.
damd
Ce n'est pas une raison pour voter contre. Les gens abusent de ce privilège
Winnemucca
11
 scope: false
 transclude: false

et vous aurez la même portée (avec l'élément parent)

$scope.$watch(...

Il existe de nombreuses façons d'accéder à la portée parent en fonction de ces deux options, portée et transclude.

Stepan Suvorov
la source
Oui, court et doux, et correct. Cependant, ils semblent partager exactement la même portée que l'élément parent ... ce qui les rend impossibles à réutiliser dans la même portée. jsfiddle.net/collindo/xqytH
colllin
2
plusieurs fois, nous avons besoin d'une portée isolée lorsque nous écrivons un composant réutilisable, donc la solution n'est pas aussi simple
Yvon Huynh
8

Voici une astuce que j'ai utilisée une fois: créer une directive "factice" pour contenir la portée parent et la placer quelque part en dehors de la directive souhaitée. Quelque chose comme:

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

puis

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

Ce n'est peut-être pas la solution la plus gracieuse, mais elle a fait le travail.

nouveau venu
la source
4

Si vous utilisez les classes et la ControllerAssyntaxe ES6 , vous devez faire quelque chose de légèrement différent.

Voir l'extrait ci-dessous et notez qu'il vms'agit de la ControllerAsvaleur du contrôleur parent telle qu'elle est utilisée dans le code HTML parent

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

        scope.vm.func(...)
Simon H
la source
0

Après avoir tout essayé, j'ai finalement trouvé une solution.

Placez simplement ce qui suit dans votre modèle:

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

Il écrit simplement l'attribut / variable de portée parent auquel vous souhaitez accéder à la portée actuelle.

Notez également le ; ''à la fin de l'instruction, c'est pour vous assurer qu'il n'y a pas de sortie dans votre modèle. (Angular évalue chaque instruction, mais ne produit que la dernière).

C'est un peu hacky, mais après quelques heures d'essais et d'erreurs, il fait l'affaire.

Jeffrey Roosendaal
la source