¿Cómo acceder al ámbito principal desde una directiva personalizada * con ámbito propio * en AngularJS?

327

Estoy buscando cualquier forma de acceder al alcance "principal" dentro de una directiva. Cualquier combinación de alcance, transcluir, requerir, pasar variables (o el alcance en sí) desde arriba, etc. Estoy totalmente dispuesto a inclinarme hacia atrás, pero quiero evitar algo totalmente hacky o imposible de mantener. Por ejemplo, sé que podría hacerlo en este momento tomando los $scopeparámetros de preLink e iterando sobre sus $siblingámbitos para encontrar el "padre" conceptual.

Lo que realmente quiero es poder $watchuna expresión en el ámbito primario. Si puedo hacer eso, entonces puedo lograr lo que estoy tratando de hacer aquí: AngularJS: ¿cómo representar un parcial con variables?

Una nota importante es que la directiva debe ser reutilizable dentro del mismo ámbito principal. Por lo tanto, el comportamiento predeterminado (alcance: falso) no funciona para mí. Necesito un ámbito individual por instancia de la directiva, y luego necesito $watchuna variable que viva en el ámbito primario.

Una muestra de código vale 1000 palabras, entonces:

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
fuente

Respuestas:

644

Ver ¿Cuáles son los matices de alcance prototípico / herencia prototípica en AngularJS?

Para resumir: la forma en que una directiva accede a su ámbito primario ( $parent) depende del tipo de ámbito que crea la directiva:

  1. default ( scope: false): la directiva no crea un nuevo ámbito, por lo que no hay herencia aquí. El alcance de la directiva es el mismo alcance que el padre / contenedor. En la función de enlace, use el primer parámetro (típicamente scope).

  2. scope: true- la directiva crea un nuevo ámbito secundario que hereda prototípicamente del ámbito primario. Las propiedades que se definen en el ámbito primario están disponibles para la directiva scope(debido a la herencia prototípica). Solo tenga cuidado de escribir en una propiedad de alcance primitiva, que creará una nueva propiedad en el alcance de la directiva (que oculta / sombrea la propiedad de alcance principal del mismo nombre).

  3. scope: { ... }- la directiva crea un nuevo aislamiento / alcance aislado. No hereda prototípicamente el ámbito primario. Todavía puede acceder al ámbito principal utilizando $parent, pero esto normalmente no se recomienda. En su lugar, usted debe especificar qué propiedades ámbito primario (y / o función) las necesidades directiva a través de atributos adicionales en el mismo elemento que se utiliza la directiva, con el =, @y &la notación.

  4. transclude: true- la directiva crea un nuevo ámbito secundario "transcluido", que hereda prototípicamente del ámbito primario. Si la directiva también crea un ámbito de aislamiento, los ámbitos transcluidos y los de aislamiento son hermanos. La $parentpropiedad de cada ámbito hace referencia al mismo ámbito primario.
    Actualización angular v1.3 : si la directiva también crea un ámbito de aislamiento, el ámbito transcluido ahora es un elemento secundario del ámbito de aislamiento. Los ámbitos transcluidos y aislados ya no son hermanos. La $parentpropiedad del alcance transcluido ahora hace referencia al alcance aislado.

El enlace de arriba tiene ejemplos e imágenes de los 4 tipos.

No puede acceder al alcance en la función de compilación de la directiva (como se menciona aquí: https://github.com/angular/angular.js/wiki/Understanding-Directives ). Puede acceder al alcance de la directiva en la función de enlace.

Acecho:

Para 1. y 2. arriba: normalmente especifica qué propiedad principal necesita la directiva a través de un atributo, luego $ mira:

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

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

Si está viendo una propiedad de objeto, deberá usar $ parse:

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

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

Para 3. arriba (aislar alcance), mire el nombre que le da a la propiedad de directiva usando la notación @o =:

<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
fuente
1
GRACIAS Mark. Resulta que la solución que publiqué en Cómo representar un parcial con variables realmente funciona muy bien. Lo que realmente necesitabas para vincularme era algo titulado "Los matices de escribir HTML y reconocer que tu elemento no está anidado dentro del controlador ng que crees que es". Wow ... error de novato. Pero esta es una adición útil a su otra respuesta (mucho más larga) que explica los ámbitos.
colllin
@collin, genial, me alegro de que haya resuelto su problema, ya que no estaba muy seguro de cómo responder a su otro comentario (ahora eliminado).
Mark Rajcok
¿Qué cosas puedo / debo realizar dentro?scope.$watch('localName3', function() { ...[?? WHAT TO DO HERE for example?] });
Junaid Qadir
1
@ Andy, no, no lo uses $parsecon =: violín . $parsesolo se necesita con ámbitos no aislados.
Mark Rajcok
1
Esta es una gran respuesta, muy completa. También ilustra por qué simplemente odio trabajar con AngularJS.
John Trichereau
51

Acceder al método del controlador significa acceder a un método en el alcance principal desde el controlador / enlace / alcance de la directiva.

Si la directiva comparte / hereda el ámbito primario, entonces es bastante sencillo invocar un método de ámbito primario.

Se requiere un poco más de trabajo cuando desea acceder al método de ámbito primario desde el ámbito de directiva aislado.

Hay pocas opciones (pueden ser más que las que se enumeran a continuación) para invocar un método de alcance primario desde el alcance de directivas aisladas o ver variables de alcance primario ( opción # 6 especialmente).

Tenga en cuenta que lo utilicé link functionen estos ejemplos, pero también puede utilizarlo directive controllersegún los requisitos.

Opción 1. A través del objeto literal y de la plantilla html de la directiva

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

});

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

Opcion 2. A través del objeto literal y del enlace / alcance de la directiva

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

trabajo plnkr: http://plnkr.co/edit/BRvYm2SpSpBK9uxNIcTa?p=preview

Opción # 3. A través de la referencia de funciones y de la plantilla de directiva 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"
    }]
});

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

Opción # 4. A través de la referencia de funciones y del enlace / alcance de la directiva

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

});

trabajo plnkr: http://plnkr.co/edit/BSqx2J1yCY86IJwAnQF1?p=preview

Opción n. ° 5: a través de ng-model y enlace bidireccional, puede actualizar las variables de ámbito principal. . Por lo tanto, es posible que no necesite invocar funciones de ámbito principal en algunos 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"
    }]
});

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

Opción # 6: a través $watchy$watchCollection es vinculante itemsen ambos sentidos en todos los ejemplos anteriores, si los elementos se modifican en el ámbito principal, los elementos en la directiva también reflejarían los cambios.

Si desea ver otros atributos u objetos desde el ámbito primario, puede hacerlo utilizando $watchy $watchCollectioncomo se indica a continuación

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

aplicación var = 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"
  }]
});

Siempre puede consultar la documentación de AngularJs para obtener explicaciones detalladas sobre las directivas.

Yogesh Manware
fuente
10
Trabaja duro por su representante ... tan duro por su representante ... trabaja duro por su representante, así que es mejor que lo votes bien.
delgado
77
rechazado - cualquier información valiosa dentro de la respuesta es inaccesible debido a su extensión
corrija el
2
Respondí la pregunta con todas las alternativas disponibles con una separación clara. En mi opinión, las respuestas cortas no siempre son útiles hasta que tengas una visión general frente a ti.
Yogesh Manware
@YogeshManware: podría acortarse mucho omitiendo las cosas irrelevantes, como hojas de estilo, sin usar marcado largo, simplificando los ejemplos para no usar cosas como "agrupar por", etc. También sería muy útil con algún tipo de explicación para cada ejemplo
maldito
Esta no es una razón para rechazar el voto. La gente abusa de este privilegio
Winnemucca
11
 scope: false
 transclude: false

y tendrá el mismo alcance (con elemento primario)

$scope.$watch(...

Hay muchas formas de acceder al ámbito principal en función de estas dos opciones de ámbito y transcluir.

Stepan Suvorov
fuente
Sí, corto y dulce, y correcto. Sin embargo, parecen compartir exactamente el mismo alcance que el elemento padre ... lo que hace que sea imposible reutilizarlos en el mismo alcance. jsfiddle.net/collindo/xqytH
colllin
2
muchas veces necesitamos un alcance aislado cuando escribimos un componente reutilizable, por lo que la solución no es tan simple
Yvon Huynh
8

Aquí hay un truco que utilicé una vez: crear una directiva "ficticia" para mantener el alcance principal y colocarlo en algún lugar fuera de la directiva deseada. 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...
        }
    };
});

y entonces

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

Quizás no sea la solución más elegante, pero hizo el trabajo.

recién llegado
fuente
4

Si está utilizando las clases y la ControllerAssintaxis de ES6 , debe hacer algo ligeramente diferente.

Consulte el fragmento a continuación y observe que ese vmes el ControllerAsvalor del controlador principal tal como se utiliza en el HTML principal

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

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

Después de haber intentado todo, finalmente se me ocurrió una solución.

Simplemente coloque lo siguiente en su plantilla:

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

Simplemente escribe el atributo / variable de alcance primario al que desea acceder en el alcance actual.

También ; ''tenga en cuenta que al final de la declaración, es para asegurarse de que no hay salida en su plantilla. (Angular evalúa cada declaración, pero solo genera la última).

Es un poco hacky, pero después de algunas horas de prueba y error, hace el trabajo.

Jeffrey Roosendaal
fuente