Como acessar o escopo pai de dentro de uma diretiva personalizada * com escopo próprio * no AngularJS?

327

Estou procurando qualquer maneira de acessar o escopo "pai" dentro de uma diretiva. Qualquer combinação de escopo, transcluir, exigir, passar variáveis ​​(ou o próprio escopo) de cima, etc. Estou totalmente disposta a reverter, mas quero evitar algo totalmente hacky ou inatingível. Por exemplo, eu sei que poderia fazê-lo agora pegando os $scopeparâmetros do preLink e iterando sobre seus $siblingescopos para encontrar o "pai" conceitual.

O que eu realmente quero é ser capaz de $watchuma expressão no escopo pai. Se eu puder fazer isso, posso realizar o que estou tentando fazer aqui: AngularJS - Como renderizar uma parcial com variáveis?

Uma observação importante é que a diretiva deve ser reutilizável dentro do mesmo escopo pai. Portanto, o comportamento padrão (escopo: false) não funciona para mim. Preciso de um escopo individual por instância da diretiva e, em seguida, preciso de $watchuma variável que viva no escopo pai.

Uma amostra de código vale 1.000 palavras, portanto:

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
fonte

Respostas:

644

Consulte Quais são as nuances da herança prototípica / prototípica do escopo no AngularJS?

Resumindo: a maneira como uma diretiva acessa seu $parentescopo pai ( ) depende do tipo de escopo que a diretiva cria:

  1. default ( scope: false) - a diretiva não cria um novo escopo; portanto, não há herança aqui. O escopo da diretiva é o mesmo que o pai / contêiner. Na função de link, use o primeiro parâmetro (normalmente scope).

  2. scope: true- a diretiva cria um novo escopo filho que herda prototipicamente do escopo pai. As propriedades definidas no escopo pai estão disponíveis para a diretiva scope(devido à herança prototípica). Apenas tome cuidado ao escrever em uma propriedade de escopo primitiva - que criará uma nova propriedade no escopo da diretiva (que oculta / oculta a propriedade do escopo pai com o mesmo nome).

  3. scope: { ... }- a diretiva cria um novo escopo isolado / isolado. Ele não herda prototipicamente o escopo pai. Você ainda pode acessar o escopo pai usando $parent, mas isso normalmente não é recomendado. Em vez disso, você deve especificar quais âmbito propriedades pai (e / ou função) as necessidades directiva por via de atributos adicionais sobre o mesmo elemento em que a directiva é utilizado, usando o =, @e &notação.

  4. transclude: true- a diretiva cria um novo escopo filho "transcluído", que herda prototipicamente do escopo pai. Se a diretiva também criar um escopo isolado, os escopos transcluído e isolado são irmãos. A $parentpropriedade de cada escopo faz referência ao mesmo escopo pai.
    Atualização angular v1.3 : se a diretiva também criar um escopo isolado, o escopo transcluído será agora filho do escopo isolado. Os escopos transcluídos e isolados não são mais irmãos. A $parentpropriedade do escopo transcluído agora faz referência ao escopo isolado.

O link acima tem exemplos e imagens dos 4 tipos.

Você não pode acessar o escopo na função de compilação da diretiva (conforme mencionado aqui: https://github.com/angular/angular.js/wiki/Understanding-Directives ). Você pode acessar o escopo da diretiva na função link.

Assistindo:

Para 1. e 2. acima: normalmente você especifica de qual propriedade pai a diretiva precisa por meio de um atributo, e então $ watch:

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

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

Se você estiver assistindo a uma propriedade de objeto, precisará usar $ parse:

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

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

Para 3. acima (isolar escopo), observe o nome que você fornece à propriedade diretiva usando a notação @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
fonte
1
OBRIGADO, Mark. Acontece que a solução que eu postei em Como renderizar um parcial com variáveis realmente funciona muito bem. O que você realmente precisava me vincular era algo intitulado "As nuances de escrever HTML e reconhecer que seu elemento não está aninhado dentro do ng-controller que você pensa que é". Uau ... erro de novato. Mas essa é uma adição útil à sua outra resposta (muito mais longa) que explica os escopos.
colllin
@ collin, ótimo, fico feliz que você tenha resolvido o seu problema, pois não tinha certeza de como responder ao seu outro comentário (agora excluído).
Mark Rajcok
Que material posso / devo executar dentro?scope.$watch('localName3', function() { ...[?? WHAT TO DO HERE for example?] });
Junaid Qadir
1
@ Andy, não, não use $parsecom =: violino . $parseé necessário apenas com escopos não isolados.
Mark Rajcok
1
Esta é uma ótima resposta, muito completa. Também ilustra por que eu simplesmente odeio trabalhar com o AngularJS.
John Trichereau
51

Acessar o método do controlador significa acessar um método no escopo pai a partir da diretiva controller / link / scope.

Se a diretiva estiver compartilhando / herdando o escopo pai, é bastante simples invocar um método de escopo pai.

Um pouco mais de trabalho é necessário quando você deseja acessar o método de escopo pai no escopo da diretiva Isolado.

Existem poucas opções (podem ser mais do que as listadas abaixo) para invocar um método de escopo pai a partir do escopo de diretivas isoladas ou observar variáveis ​​de escopo pai ( especialmente a opção nº 6 ).

Observe que usei link functionnesses exemplos, mas você também pode usar um directive controllercom base nos requisitos.

Opção 1. Através do objeto literal e do modelo html de diretiva

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

});

trabalhando plnkr: http://plnkr.co/edit/rgKUsYGDo9O3tewL6xgr?p=preview

Opção 2. Através do objeto literal e do link / escopo da diretiva

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 trabalho: http://plnkr.co/edit/BRvYm2SpSpBK9uxNIcTa?p=preview

Opção # 3. Através da referência de função e do modelo de diretiva 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 trabalho: http://plnkr.co/edit/Jo6FcYfVXCCg3vH42BIz?p=preview

Opção # 4. Através da referência de função e do link / escopo da diretiva

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 trabalho: http://plnkr.co/edit/BSqx2J1yCY86IJwAnQF1?p=preview

Opção 5: através do modelo ng e da ligação bidirecional, você pode atualizar as variáveis ​​do escopo pai. . Portanto, talvez não seja necessário chamar funções de escopo pai em alguns casos.

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

trabalhando plnkr: http://plnkr.co/edit/hNui3xgzdTnfcdzljihY?p=preview

Opção 6: Complementar $watche$watchCollection É de duas vias, itemsnos exemplos acima, se os itens forem modificados no escopo pai, os itens na diretiva também refletirão as alterações.

Se você quiser assistir outros atributos ou objetos do escopo pai, poderá fazer isso usando $watche $watchCollectioncomo indicado abaixo

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

Você sempre pode consultar a documentação do AngularJs para obter explicações detalhadas sobre diretivas.

Yogesh Manware
fonte
10
Ele trabalha duro para o seu representante ... tão duro para o seu representante ... ele trabalha duro para o seu representante, então é melhor você votá-lo direito.
fino
7
downvoted - nenhuma informação valiosa dentro da resposta é inacessível devido ao seu comprimento
reparação
2
Respondi à pergunta com todas as alternativas disponíveis com clara separação. Na minha opinião, respostas curtas nem sempre são úteis até que você tenha uma visão geral à sua frente.
Yogesh Manware
@YogeshManware: poderia ser muito reduzido, deixando de fora coisas irrelevantes, como folhas de estilo, sem usar marcações longas, simplificando os exemplos para não usar coisas como "agrupar por", etc. Também seria muito útil com algum tipo de explicação para cada exemplo.
039
Este não é um motivo para votar. As pessoas abusam deste privilégio #
Winnemucca
11
 scope: false
 transclude: false

e você terá o mesmo escopo (com elemento pai)

$scope.$watch(...

Existem várias maneiras de acessar o escopo pai, dependendo dessas duas opções escopo e transcluir.

Stepan Suvorov
fonte
Sim, curto e doce, e correto. Eles parecem compartilhar exatamente o mesmo escopo que o elemento pai ... o que os torna impossíveis de reutilizar no mesmo escopo. jsfiddle.net/collindo/xqytH
colllin
2
muitas vezes precisamos isolado âmbito Quando escrevemos componente reutilizável, para que a solução não é tão simples
Yvon Huynh
8

Aqui está um truque que usei uma vez: crie uma diretiva "fictícia" para manter o escopo pai e coloque-o em algum lugar fora da diretiva desejada. Algo como:

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

e depois

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

Talvez não seja a solução mais elegante, mas conseguiu o trabalho.

de novo
fonte
4

Se você estiver usando classes e ControllerAssintaxe do ES6 , precisará fazer algo um pouco diferente.

Veja o trecho abaixo e observe que esse vmé o ControllerAsvalor do controlador pai, conforme usado no HTML pai

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

        scope.vm.func(...)
Simon H
fonte
0

Tendo tentado de tudo, finalmente cheguei a uma solução.

Basta colocar o seguinte no seu modelo:

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

Ele apenas grava o atributo / variável do escopo pai que você deseja acessar no escopo atual.

Observe também que, ; ''no final da instrução, é para garantir que não haja saída no seu modelo. (Angular avalia todas as instruções, mas apenas produz a última).

É um pouco hacky, mas depois de algumas horas de tentativa e erro, ele faz o trabalho.

Jeffrey Roosendaal
fonte