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


Estoy buscando cualquier forma de acceder al ámbito "padre" dentro de una directiva. Cualquier combinación de scope, transclude, require, pasando variables (o el propio scope) desde arriba, etc. Estoy totalmente dispuesto a hacer lo imposible, pero quiero evitar algo totalmente hackeado o inalcanzable. Por ejemplo, sé que podría hacerlo ahora mismo tomando $scope de los parámetros de preLink e iterando sobre sus ámbitos $sibling para encontrar el "padre"conceptual.

Lo que realmente quiero es ser capaz de $watch una expresión en el ámbito padre. Si puedo hacer eso, entonces puedo lograr lo que estoy tratando de hacer aquí: AngularJS - ¿Cómo renderizar un parcial con variables?

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

Un ejemplo de código vale 1000 palabras, así que:

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?
                }
            }
        }
    };
});
Author: Community, 2013-07-27

6 answers

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

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

  1. Default (scope: false) - la directiva no crea un nuevo ámbito, por lo que no hay herencia aquí. El ámbito de aplicación de la directiva es el mismo que el principal/contenedor. En la función de enlace, utilice el primer parámetro (normalmente 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 padre están disponibles para la directiva scope (debido a la herencia de prototipos). Tenga cuidado de escribir en una propiedad de ámbito primitivo that que creará una nueva propiedad en el ámbito de la directiva (que oculta/sombrea la propiedad de ámbito padre del mismo nombre).

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

  4. transclude: true - la directiva crea un nuevo ámbito secundario "transcluido", que prototípicamente hereda del ámbito padre. Si la directiva también crea un ámbito aislado, los ámbitos transcluded y isolate son hermanos. La propiedad $parent de cada ámbito hace referencia al mismo ámbito padre.
    Angular v1. 3 update: Si la directiva también crea un ámbito aislado, el ámbito transcluded ahora es hijo del ámbito aislado. Los ámbitos transcluded y isolate ya no son hermanos. La propiedad $parent del ámbito transcluido ahora hace referencia al aislamiento alcance.

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

No se puede acceder al ámbito 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 ámbito de la directiva en la función de enlace.

Viendo:

Para 1. y 2. arriba: normalmente se especifica qué propiedad padre necesita la directiva a través de un atributo, luego watch watch it:

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

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

Si está viendo una propiedad de objeto, necesitará usar par parse:

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

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

Para 3. arriba (ámbito de aislamiento), observe el nombre que le da a la propiedad 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() { ... });
 618
Author: Mark Rajcok,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-23 11:55:03

Acceder al método del controlador significa acceder a un método en el ámbito padre desde la directiva controller/link/scope.

Si la directiva está compartiendo/heredando el ámbito padre, entonces es bastante sencillo invocar un método de ámbito padre.

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

Hay pocas opciones (pueden ser más de las enumeradas a continuación) para invocar un método de ámbito padre desde directivas aisladas scope o ver variables de ámbito padre ( opción#6 especialmente).

Tenga en cuenta que usé link function en estos ejemplos, pero también puede usar un directive controller en función de los requisitos.

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

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

Opción # 2. A través del objeto literal y desde la directiva 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="[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 trabajo: http://plnkr.co/edit/BRvYm2SpSpBK9uxNIcTa?p=preview

Opción # 3. A través de referencia de función y desde la directiva html plantilla

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

Opción # 4. A través de la referencia de función y desde la directiva 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="[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 trabajo: http://plnkr.co/edit/BSqx2J1yCY86IJwAnQF1?p=preview

Opción # 5: A través del modelo ng y el enlace bidireccional, puede actualizar las variables de ámbito padre.. Por lo tanto, es posible que no necesite invocar funciones de ámbito padre 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"
    }]
});

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

Opción # 6: A través de $watch y $watchCollection Es bidireccional para items en todos los ejemplos anteriores, si los elementos se modifican en el ámbito padre, los elementos en la directiva también reflejarían los cambios.

Si desea ver otros atributos u objetos del ámbito padre, puede hacerlo utilizando $watch y $watchCollection como 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>

Aplicación de script.js

Var app = angular.módulo ('émbolo', []);

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 puedes referir AngularJS documentación para explicaciones detalladas sobre directivas.

 48
Author: Yogesh Manware,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-01-17 12:04:33
 scope: false
 transclude: false

Y tendrá el mismo ámbito(con el elemento padre)

$scope.$watch(...

Hay muchas maneras de acceder al ámbito padre dependiendo de estas dos opciones scope& transclude.

 12
Author: Stepan Suvorov,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-07-27 17:19:56

He aquí un truco que utilicé una vez: crear una directiva "ficticia" para mantener el ámbito padre 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 luego

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

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

 7
Author: anewcomer,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-11-15 01:42:56

Si está utilizando clases ES6 y ControllerAs sintaxis , necesita hacer algo ligeramente diferente.

Vea el fragmento de código a continuación y tenga en cuenta que vm es el valor ControllerAs del Controlador padre como se usa en el HTML padre

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

        scope.vm.func(...)
 4
Author: Simon H,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-01-08 10:53:03

Habiendo probado 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 ámbito principal al que desea acceder al ámbito actual.

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

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

 0
Author: Jeffrey Roosendaal,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-07-25 10:42:15