Angular.js: ng-transclude no debe crear un nuevo alcance de hermanos.

Creado en 20 dic. 2013  ·  69Comentarios  ·  Fuente: angular/angular.js

Esta es más una solicitud de cambio y me gustaría ver qué piensan otras personas.

En mi humilde opinión, ng-transclude no debería crear su propio alcance o al menos tener una forma de evitar que lo haga. La razón detrás de esto es que una directiva que solicita la transclusión ya tiene medios para especificar si quiere o no tener un alcance o un alcance aislado o ningún alcance. Utiliza la directiva ng-transclude para marcar dónde desea insertar el contenido. Cuando ng-transclude crea su propio alcance hermano, rompe las expectativas de la directiva que define qué tipo de alcance quiere y surge la manifestación de la confusión popular entre 'valor' vs 'objeto.valor'.

Aquí hay un ejemplo de dónde un nuevo alcance no tiene sentido en mi opinión:

ui.directive('box', function() {
    return {
        restrict: 'E',
        transclude: true,
        template: '<div ng-transclude/>',
        replace: true,
        scope: {}
    };
});

Todo lo que esta directiva quiere es reemplazar <box>content</box> con <div>content</div> y que el contenido tenga el alcance aislado.

La creación de una estructura anidada de directivas como esta conduce a una contaminación del árbol de alcance. Aquí hay un ejemplo de plunker (http://plnkr.co/edit/DwukVGGprFFjQuVY8yTz) de tres directivas anidadas que crean una estructura de árbol de alcance como esta:

< Scope (002) : ng-app
    < Scope (003) : ng-controller
        < Scope (004) : box
        < Scope (005) : ng-transclude
            < Scope (006) : box
            < Scope (007) : ng-transclude
                < Scope (008) : box
                < Scope (009) : ng-transclude

Este comportamiento no parece agregar ningún valor a su propósito, pero crea mucha confusión entre los principiantes.

Por el momento, utilizo la siguiente solución alternativa que logra exactamente lo que hace el ejemplo anterior:

ui.directive('box', function() {
    return {
        restrict: 'E',
        transclude: true,
        template: '<div/>',
        replace: true,
        scope: {},
        link: function(scope, element, attrs, transclude) {
            transclude(scope.$parent, function(content) {
                element.append(content);
            });
        }
    };
});

Aquí hay un ejemplo de plunker (http://plnkr.co/edit/46v6IBLkhS71L1WbUDFl) que ilustra este concepto. Deja el árbol del alcance agradable y ordenado:

< Scope (002) : ng-app
    < Scope (003) : ng-controller
        < Scope (004) : box
        < Scope (005) : box
        < Scope (006) : box

Y la vinculación bidireccional funciona de la manera que muchos esperan cuando vinculan 'valor' en lugar de 'objeto.valor'. (Creo que el hecho de que pasar solo 'valor' funciona en algunos casos pero no en el otro y culpar a la naturaleza de la herencia prototípica en javascript no es una buena excusa. El hecho de que muchas personas encuentren este comportamiento inesperado indica que hay una falla arquitectónica .)

Me encantaría escuchar lo que otras personas piensan y casos de uso en los que piensan que tiene sentido crear un nuevo ámbito hermano para ng-transclude.

Lots of comments $compile high won't fix bug

Comentario más útil

Menos mal que me cambié a Ember hace años. :)

Todos 69 comentarios

¿Dónde transcluir crea un nuevo alcance? http://plnkr.co/edit/EuHaBR26JgAegQKvwOGH?p=preview No lo veo

Estoy hablando de la directiva ng-transclude. Lo que tienes en tu ejemplo es exactamente lo que hace mi trabajo.

esta es una solicitud válida. Estábamos considerando esto para la versión 1.2, pero se acercaba a la versión final y no queríamos introducir este cambio radical.

deberíamos considerarlo por 1.3

¡Bonita! Me alegro de que ya lo hayas estado considerando.

+1 para esto. Creo que mi problema está relacionado con esto: uso ng-transclude en una directiva con formularios y tengo que pasar por el alcance. $$ childHead para acceder al objeto de validación del formulario, pero no tengo ningún problema para acceder a mis modelos.

Aquí hay un ejemplo: http://fiddle.jshell.net/39cgW/3/

+1 me encuentro con este problema hoy y odio tirar $parent todas partes.

Entonces, para encontrar una solución para esto, parece que hay dos posibilidades

1) modifique la directiva ngTransclude para especificar su alcance (en realidad, podría reducirse mucho más que esto, creo que no se necesita controlador)

o

2) no cree un nuevo alcance cuando el alcance no está especificado

Entonces, podríamos ahorrar algunos bytes con la opción 1) y eso es bueno, 2) sería la solución más pequeña (eliminación de 3 líneas más o menos) y no me queda claro que haya casos de uso en los que crear un nuevo alcance implícitamente tenga sentido allí ( tal vez los haya, pero parece completamente contrario a la forma en que se describe la transclusión en los documentos)


O, si quisiera ser súper elegante, tal vez podría evitar romper los cambios por completo permitiendo que ngTransclude especifique que quiere un nuevo alcance o no, a través del valor del atributo.

¿Pensamientos?

¡No entiendo la diferencia entre tu 1 y 2!

Solo mi opinión, pero creo que la compatibilidad con versiones anteriores será importante para este cambio. Me imagino que hay muchas aplicaciones (incluida la mía) que han solucionado este problema de alcance usando cosas como $ parent y scope. $$ childHead. Cualquier actualización que altere este comportamiento causará algunos dolores de cabeza (pero quizás sea mejor causar dolores de cabeza más temprano que tarde).

Dicho esto, desde un punto de vista teórico, creo que tiene más sentido que ng-tranclude tenga el mismo alcance que la directiva por defecto. El punto de trasladar contenido es que lo desea como parte integrante del contenido. Hay ocasiones en las que tengo muchas directivas anidadas con transcluidas, pero aún quiero que se comporten como un componente grande. Tenerlos con todos los ámbitos diferentes hace que esto sea muy complicado.

Todos solo mis pensamientos. Como mínimo, tener la opción será un paso por encima de la situación actual. :)

@troch para aclarar,

        // This is the function that is injected as `$transclude`.
        function controllersBoundTransclude(scope, cloneAttachFn) {
          var transcludeControllers;

          // no scope passed
          if (arguments.length < 2) {
            cloneAttachFn = scope;
            scope = undefined;
          }

          if (hasElementTranscludeDirective) {
            transcludeControllers = elementControllers;
          }

          return boundTranscludeFn(scope, cloneAttachFn, transcludeControllers);
        }

La directiva llama a esta función sin alcance y, como tal, el alcance no está definido ... Luego, en boundTranscludeFn , crea un nuevo alcance si transcludeScope es falso ...

Entonces, lo que estoy diciendo es que, para 1), simplemente podemos especificar el alcance actual para la función de transcluir (dado que esta directiva es una directiva vecina de cualquier cosa que pueda tener un alcance aislado, esto aún debería darnos el alcance original) .

Alternativamente, 2), no cree un nuevo alcance y simplemente por defecto al alcance actual (en createBoundTranscludeFn) (cambio potencialmente rompiente y potencialmente rompiendo muchas pruebas).

Ambos son bastante sencillos de hacer.

+1

+1

+1

Definitivamente +1

+1

+1

+1

+1 por favor

Ya sabes lo que sería bastante bueno, aunque tal vez no sea totalmente factible a tiempo para 1.3, los proxies ES6 podrían hacer que la transclusión sea realmente agradable, manteniéndose al día con las propiedades del alcance de la transclusión, pero teniendo la posición correcta en la jerarquía.

Si la implementación de Proxy no está disponible, probablemente podría recurrir al alcance. $ New (), por lo que en realidad podría ser posible hacer que esto funcione bastante pronto. Lo complicado es que la especificación es un poco inestable.

Por lo tanto, aún obtendría alcances no deseados si desea que la jerarquía de alcance sea muy limpia, pero al menos tendrá el efecto secundario de que los enlaces de datos no se rompan inesperadamente. No se.

+1

+1

+1

+1

+1

@caitp

O, si quisiera ser súper elegante, tal vez podría evitar romper los cambios por completo permitiendo que ngTransclude especifique que quiere un nuevo alcance o no, a través del valor del atributo.

Todo lo que no tenga cambios importantes recibe mi voto.

+1

+1

+1

+1

+1

+1. Siempre me pregunté sobre este comportamiento. Me gusta esta solución de caitp:

@caitp

O, si quisiera ser súper elegante, tal vez podría evitar romper los cambios por completo permitiendo que> ngTransclude especifique que quiere un nuevo alcance o no, a través del valor del atributo.

+1

+1

+1

Para aquellos que están viendo este problema, he creado una directiva ng-transclude 'mejorada', cuyo valor de atributo define el alcance interno buscado y puede ser uno de 3:

  • silbing : el alcance del contenido transcluido es hermano del elemento donde ocurre la transclusión. Ese es el comportamiento actual de ng-transcluir.
  • parent : el alcance del contenido transcluido es el del elemento donde ocurre la transclusión.
  • child : el alcance del contenido transcluido es un alcance secundario del alcance del elemento donde ocurre la transclusión.

Uso de ejemplo:

template: 
  '<div ng-transclude="parent">' +
  '</div>'

Ejemplo completo

Vea este plunk para un ejemplo de todos.

La salida se ve así:

image

Fuente

.config(function($provide){
    $provide.decorator('ngTranscludeDirective', ['$delegate', function($delegate) {
        // Remove the original directive
        $delegate.shift();
        return $delegate;
    }]);
})

.directive( 'ngTransclude', function() {
  return {
    restrict: 'EAC',
    link: function( $scope, $element, $attrs, controller, $transclude ) {
      if (!$transclude) {
        throw minErr('ngTransclude')('orphan',
         'Illegal use of ngTransclude directive in the template! ' +
         'No parent directive that requires a transclusion found. ' +
         'Element: {0}',
         startingTag($element));
      }

      var iScopeType = $attrs['ngTransclude'] || 'sibling';

      switch ( iScopeType ) {
        case 'sibling':
          $transclude( function( clone ) {
            $element.empty();
            $element.append( clone );
          });
          break;
        case 'parent':
          $transclude( $scope, function( clone ) {
            $element.empty();
            $element.append( clone );
          });
          break;
        case 'child':
          var iChildScope = $scope.$new();
          $transclude( iChildScope, function( clone ) {
            $element.empty();
            $element.append( clone );
            $element.on( '$destroy', function() {
              iChildScope.$destroy();
            });            
          });
          break;
      }
    }
  }
})

Como el problema que mencioné anteriormente en el n. ° 8609 se cerró como un duplicado de este hilo, lo estoy reafirmando aquí.

En mi opinión, la forma actual en que se crea un alcance para la parte transcluida del DOM es muy ilógica.
Va en contra del flujo normal en angular, ¡y debe arreglarse!

Aquí hay un extracto de mi número anterior:

Creé un pequeño plunk para ilustrar mi problema aquí.

el principal infractor está en esta directiva

     function pane() {
        return {
           restrict: 'E',
           transclude: true,
           scope: {
              title: '@'
           },
           template: '<div style="border: 1px solid black;">' +
              '<div style="background-color: gray">{{title}} (isolate scope id: {{$id}})</div>' +
              '<ng-transclude></ng-transclude>' +
              '</div>'
        };
     }

Cuando un usuario hace algo como esto:

<form>
    <pane title='enter your name'>
         <input type='text ngModel='username'>
    </pane>
    <pane title='enter your token'>
         <input type='text ngModel='token'>
   </pane>


El resultado sorprenderá a muchos, especialmente a los nuevos usuarios. Y este es incluso un caso de uso (demasiado) simplificado. Intente poner en la pantalla algunos mensajes de validación allí;)

Este es un caso de uso diferente al que comenzó con este problema, ¡pero estoy de acuerdo en que básicamente es el mismo problema!

En su lugar, tiene mucho más sentido transcluir el alcance con el elemento dom. Si realmente se necesita un nuevo alcance, el usuario puede poner un ngController en el elemento ngTransclude todos modos. O puede haber una bandera opcional en ngTransclude que active una. Esa bandera también se puede usar para resolver el caso de uso # 5489. Y también se puede utilizar para la solución que ofrece caitp.

+1

+1

+1

+1

+1

+1

+1

+1

+1

+1

+1

+1

+1 parece que la directiva transcluida crea un alcance incluso cuando se le pide que no lo haga. ejemplo http://plnkr.co/edit/Wn81IBkE87vtigXvjmIa?p=preview

+1

+1

+1

+1

+1

+1

+1

+1

+ 1: ¿existen soluciones alternativas válidas para esto?

@nikkwong , hay un par de soluciones publicadas en este hilo. Sé que lo vinculé en

De acuerdo, incluso si esto llega, no será hasta 1.5.x

Pero antes de eso tengo una preocupación. La razón principal para crear un nuevo alcance (llámelo un hermano del alcance aislado, o más exactamente un hijo del alcance original de donde proviene el contenido transcluido) es evitar fugas de memoria.

Compare este plunker:
http://plnkr.co/edit/3NVxdYGy1AFDvD0M2BYI?p=preview

con una versión que reutiliza el alcance de donde proviene la transclusión original:
http://plnkr.co/edit/MXFz2awcqwQQ7R882Xwz?p=preview

En la segunda versión, a medida que activa y desactiva el contenido transferido, la cantidad de observadores sigue aumentando: una pérdida de memoria.

@petebacondarwin definitivamente debería ser un nuevo alcance para que pueda ser destruido con sus observadores. Creo que la mayor parte del dolor por la transclusión se debe a que ng-transclude crearía un alcance hermano del alcance contenedor, haciendo que no sea posible acceder a sus variables a través de la herencia prototípica. ¿O me equivoco?

Nos dimos cuenta de que el problema es la forma en que funciona el enlace angular de 2 vías, definitivamente ng-transclude crea un alcance secundario, pero habrá numerosas ocasiones en las que se encontrará en la misma situación, por ejemplo, ng-repeat crea un alcance secundario. Después de mirar el código angular, nos dimos cuenta de que el problema con el enlace bidireccional no funciona con la propiedad del atributo del objeto, pero funciona bien con los objetos en sí.

Problema: http://plnkr.co/edit/Wn81IBkE87vtigXvjmIa?p=preview

Solución: http://plnkr.co/edit/KShClgQVwIjscXPzVRwR?p=preview

No es perfecto, pero sabiendo que esto resolvió muchos de nuestros problemas, nunca haga un enlace bidireccional en un atributo

Tenga en cuenta que esta solución ya fue sugerida por mbykovskyy, cuando planteó el problema, solo estoy dando un ejemplo, ya que me tomó un tiempo resolverlo.

@petebacondarwin Solo será una fuga temporal, hasta que se destruya el alcance de retención. Aquí hay (un plunk) [http://plnkr.co/edit/Q587WQnX0u0u7JjhtCxa?p=preview] que muestra que si cambia el transclude-holding-scope, todo se liberará sin problemas.
Aún así, es un punto que necesita atención. ¿Quizás un gran Waring en los documentos sobre esta posible fuga podría ser suficiente para solucionarlo?

Bueno, cualquier fuga de JS es solo temporal hasta que actualice el navegador ;-)
El 8 de septiembre de 2015 a las 16:41, "Sander Elias" [email protected] escribió:

@petebacondarwin https://github.com/petebacondarwin Solo será un
fuga temporal, hasta que se destruya el alcance de sujeción. Aquí está (un
plunk) [http://plnkr.co/edit/Q587WQnX0u0u7JjhtCxa?p=preview] que muestra
que si cambia el alcance de sujeción de transcluir, todo se pondrá
lanzado muy bien.
Aún así, es un punto que necesita atención. Quizás un gran Waring en el
docs sobre esta posible fuga podría ser suficiente para solucionarlo?

-
Responda a este correo electrónico directamente o véalo en GitHub
https://github.com/angular/angular.js/issues/5489#issuecomment -138603298
.

@SanderElias Los usuarios de nuestra aplicación Angular están ocupados desde el principio hasta el final del día.
Ya vemos que el uso de la memoria va en aumento durante el día y debemos tener mucho cuidado con lo que ponemos en la página, introducir más posibles fugas es arriesgado.

@troch : la transclusión en realidad crea un alcance secundario del alcance donde se encuentra originalmente el contenido transcluido. Esto se rompió algunas versiones atrás (definitivamente en 1.2) donde, en cambio, simplemente creó un hijo del padre del alcance de las directivas actuales. Esto significaba que las transclusiones profundamente anidadas en realidad tenían el alcance de transclusión incorrecto.

Tengo que estar de acuerdo con @petebacondarwin , e incluso cuando el comportamiento actual no es 100% intuitivo, entonces el comportamiento actual funciona mejor para evitar cualquier fuga. Me inclino a cerrar este problema ya que no lo arreglaré

Menos mal que me cambié a Ember hace años. :)

¿Fue útil esta página
0 / 5 - 0 calificaciones