Angular.js: angular 1.2.18: problema de repetición ng con transcluir

Creado en 17 jun. 2014  ·  48Comentarios  ·  Fuente: angular/angular.js

Cuando ng-transclude pasa a una directiva, el contenido html con la referencia {{item}} que desea repetir (a través de ng-repeat = "elemento en la colección" implementado en la directiva) no funciona con la versión 1.2.18

http://embed.plnkr.co/EvzF25sPD3uZLQivDFqy/preview

Comentario más útil

Lo que terminé haciendo fue usar $parent . Es el armario de la vainilla sin tener que añadir demasiadas cosas.

Entonces tengo algo como:

angular.module('test').directive('myDirectiveWithTransclusion', function() {
     return {
          restrict: 'E'
          transclude: {
               transcludeThis: 'transcludeThis'
          }
          template: "<div ng-repeat='item in array'><div ng-transclude='transcludeThis'></div></div>"
     }
})
<my-directive-with-transclusion>
     <transclude-this>
          {{$parent.item}}
     </transclude-this>
</my-directive-with-transclusion>

Todos 48 comentarios

oh Lawdy. @petebacondarwin ¿ quieres hacer otro de estos? esto es realmente el "no crear un alcance hermano con ng-transclude" de nuevo, es solo que funcionó antes para este caso debido a roturas

Lamentablemente, no es así como funciona la transclusión ng-transclude . Lo que está intentando hacer es crear una directiva de contenedor que haga uso de sus elementos secundarios como una "plantilla" de lo que se debe eliminar dentro de su propia directiva.

Me encontré con esto algunas veces en el pasado y tuve un largo debate con Misko al respecto. El contenido que se transcluye está por definición vinculado al ámbito del lugar donde se instancia la directiva; no al alcance de la plantilla de la directiva.

Anteriormente, esto puede haber funcionado ya que en realidad estábamos vinculando la transclusión al alcance incorrecto en algunos casos que involucraban escenarios de transclusión anidados.

Entonces, en realidad, no necesita usar la transclusión aquí, ya que lo que realmente intenta hacer es simplemente inyectar el HTML interno en su propia plantilla. Puede hacer esto en la función de compilación de esta manera:

http://plnkr.co/edit/j3NwMGxkVRM6QMhmydQC?p=preview

app.directive('test', function(){

  return {
    restrict: 'E',
    compile: function compile(tElement, tAttrs, tTransclude) {

      // Extract the children from this instance of the directive
      var children = tElement.children();

      // Wrap the chidren in our template
      var template = angular.element('<div ng-repeat="item in collection"></div>');
      template.append(children);

      // Append this new template to our compile element
      tElement.html('');
      tElement.append(template);

      return {
        pre: function preLink(scope, iElement, iAttrs, crtl, transclude) {
            scope.collection = [1, 2, 3, 4, 5];
        },
        post: function postLink(scope, iElement, iAttrs, controller) {
          console.log(iElement[0]);
        }
      };
    }
  };
});

Otro ejemplo de esto (creo):

Index.html:

<html ng-app='myApp'>

<head>
    <title>AngularJS Scopes</title>
    <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.1/angular.min.js"></script>
    <script src='index.js'></script>
</head>

<body ng-controller='myController'>
    <people>Hello {{person.name}}</people>
</body>
</html>

index.js:

var myApp = angular.module( 'myApp', [] );

myApp.controller( 'myController', function( $scope ) {
    $scope.people = [
        { name: 'Rob'  },
        { name: 'Alex' },
        { name: 'John' }
    ];
});

myApp.directive( 'people', function() {
    return {
        restrict: 'E',

        transclude: true,
        template: '<div ng-repeat="person in people" ng-transclude></div>',
    }
});

Trabajó con Angular 1.2.1, pero no con 1.2.18;

El desarrollador inocente solo podía esperar que el código anterior funcionara. Este documento dice:

... a veces es deseable poder pasar una plantilla completa en lugar de una cadena o un objeto. Digamos que queremos crear un componente de "cuadro de diálogo". El cuadro de diálogo debería poder ajustar cualquier contenido arbitrario.

Mientras que la documentación de ngTransclude dice:

Directiva que marca el punto de inserción para el DOM transcluido de la directiva principal más cercana que utiliza la transclusión. Cualquier contenido existente del elemento en el que se coloca esta directiva se eliminará antes de que se inserte el contenido transcluido.

¿En qué se diferencia esto de la definición de @petebacondarwin :

... crear una directiva de contenedor que haga uso de sus elementos secundarios como una "plantilla" de lo que se debe eliminar dentro de su propia directiva

Realmente no entiendo por qué la transclusión no es la solución correcta aquí. En todo caso, esperaría que la solución razonable implique inyectar el alcance de la plantilla a la función de transclusión.

La diferencia es que el contenido transcluido está ligado al "exterior", es decir, al ámbito del lugar donde se encuentra el elemento <people> .
Mientras que lo que desea es que la plantilla en línea esté vinculada al "interior", es decir, al ámbito de la directiva.
Si su directiva no crea su propio alcance, entonces esto es aproximadamente lo mismo. Si su directiva crea un alcance aislado, por ejemplo, definitivamente no es lo mismo. El alcance interno no tiene acceso al alcance externo.

Supongo que podría crear su propia directiva que de hecho inyecta el alcance de la plantilla en la función de transclusión ...

Lo que está diciendo tiene mucho sentido, pero eso es solo si comprende las profundidades de Angular. Mi punto es que el ejemplo anterior debería funcionar de alguna manera sin demasiado trabajo adicional.

Me parece muy razonable y muy práctico que la función de transclusión pueda acceder a la plantilla (o al ámbito "interno") de alguna manera. Puedo pensar en muchos casos por los que esto será necesario.

El mismo problema se explica en este blog . Creo que cada vez más personas se quejarán de cómo están las cosas en este momento (ya hay una multitud de problemas relacionados en Github como resultado de este comportamiento).

Y muchas gracias por el código. Lo estoy replicando aquí para beneficio de otros:

var myApp = angular.module( 'myApp', [] );

myApp.controller( 'myController', function( $scope ) {
    $scope.people = [
        { name: 'Rob'  },
        { name: 'Alex' },
        { name: 'John' }
    ];
});

myApp.directive( 'people', function() {
    return {
        restrict: 'E',
        transclude: true,
        template: '<div ng-repeat="person in people" inject></div>'
    }
});

myApp.directive('inject', function(){
  return {
    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 innerScope = $scope.$new();
      $transclude(innerScope, function(clone) {
        $element.empty();
        $element.append(clone);
        $element.on('$destroy', function() {
          innerScope.$destroy();
        });
      });
    }
  };
});

:-) Estoy de acuerdo en que la transclusión no es un tema fácil de entender sin mucho rascarse la cabeza y golpear el teclado.
¿Quizás necesitamos aclarar la documentación aún más en torno al hecho de que ng-transclude vinculará el contenido transcluido al alcance "externo"?

Personalmente, creo que la documentación actual, al menos en esta página fundamental , es bastante explícita y clara:

¿Qué hace exactamente esta opción de transcluir? transclude hace que el contenido de una directiva con esta opción tenga acceso al alcance fuera de la directiva en lugar de dentro.

(y todo lo que sigue ilustra esto aún más).

Consideraría agregar quizás la directiva que ha proporcionado al marco, posiblemente llamándola 'ng-transclude-internal'. Sé de al menos una persona más que intentó esto, con la directiva llamada 'transcope'.

He intentado una solución, pero me enfrenté a otro problema, por qué el alcance de los padres ng-repeat no es el alcance de la directiva

http://plnkr.co/edit/7j92IC?p=preview

@luboid, la razón por la que no funciona en su plunker es porque su directiva tiene un alcance aislado, y el DOM compilado modificado (no haga esto, por cierto, esta es una forma tonta de resolver este problema) usará un hermano del padre del alcance aislado.

Agregaré un ejemplo de una forma adecuada de hacer que funcione como espera. (Pero, este sigue siendo un diseño bastante horrible en general, no hay una buena razón para hacer esto)

En realidad, ahora que lo pienso, con ng-repeat u otras directivas de transclusión de elementos, realmente no se puede arreglar esto. Así que sí, eso no funcionará desde la versión 1.2.0

@petebacondarwin Una pregunta realmente para novatos aquí. ¿Por qué usar var innerScope lugar de solo:

myApp.directive( 'inject', function() {
    return {
        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 ));
            }

            $transclude( $scope, function( clone ) {
                $element.empty();
                $element.append( clone );
            });
        }
    };
}); 

Por lo general, es mejor crear un nuevo alcance si va a compilar algunos elementos nuevos, ya que no está seguro de si la directiva original está colocada con alguna directiva compleja que también quiera crear alcance, etc. Pero es posible que pueda obtener lejos con esto aquí ...

Gracias por la ayuda,
Usaré plantillas externas ($ templateCache), luego las cosas se vuelven un poco simples, la plantilla base se compila con el alcance de la directiva

@Izhaki @petebacondarwin Muchas gracias por incluir la directiva. Finalmente se actualizó de 1.2.16 a 1.2.20 y me tomó un poco de tiempo ver por qué mi aplicación comenzó a fallar con tanta fuerza.

Pensé que la transclusión era un concepto bastante simple antes de encontrar este hilo. No.

Gracias a todos los que han estado ayudando a aclarar esto. La actualización a 1.2.21 rompió gran parte de nuestra interfaz debido a los problemas descritos aquí, y ahora estamos en el camino correcto.

@caitp @petebacondarwin : Sería muy útil si pudiéramos obtener dos datos adicionales:

  1. ¿Qué cambio importante que ocurrió en algún lugar "sobre la versión 1.2.0" (como dijo Caitlin) resultó en que este enfoque explotara repentinamente? Entiendo lo que ya no funciona, pero no veo nada específico en el registro de cambios cerca de esa versión que parezca correlacionarse con este problema en particular.
  2. ¿Cuál es el enfoque alternativo que los usuarios finales deberían emplear en lugar de ng-transclude si queremos contenedores aislados y reutilizables de los que el contenido interno arbitrario pueda heredar? Ejemplo: una gran aplicación de múltiples inquilinos en la que incluso la interfaz es variable y está totalmente basada en datos. Entonces, tenemos cosas como controles de lista que necesitan recuperar datos para poblar ellos mismos y luego tener una interacción / comportamiento consistente. Nos gustaría poder estandarizar una directiva contenedora que proporcione esas cosas. Pero las plantillas que usamos para los elementos de la lista (que pueden contener directivas internas) varían según la situación. Y las listas a menudo se anidan, de forma recursiva, en función de la estructura de árbol de los datos que las generan. Entonces, necesitamos aislamiento para esas listas anidadas. Controlamos todo el código, por lo que no usamos la transclusión para aislar el interior del exterior. Más bien, solo buscamos un enfoque agradable y declarativo de los contenedores y su contenido arbitrario, como se recomienda explícitamente en la Guía de directivas. ¿Es ng-include la mejor opción para esto ahora (pasando la ruta a la plantilla interna como un atributo en la declaración del contenedor), a pesar de que nos aleja de las plantillas en línea y, por lo tanto, se siente un poco menos idiomático?

Entiendo lo que ya no funciona, pero no veo nada específico en el registro de cambios cerca de esa versión que parezca correlacionarse con este problema en particular.

https://github.com/angular/angular.js/blob/master/CHANGELOG.md#120 -timely-delivery-2013-11-08

Me refería a estos en particular.

  • solo pase el alcance de aislamiento a los niños que pertenecen a la directiva de aislamiento (d0efd5ee)
  • hacer aislar el alcance realmente aislar (909cabd3, # 1924, # 2500)

Pero, realmente ya no puedo recordar de qué estaba hablando, quién sabe ヽ ༼ ຈ ل͜ ຈ ༽ ノ

En el registro de cambios, solo hay dos tipos de mensajes: cambios y cambios importantes. Esto no se consideró un cambio importante, sino una corrección de errores. Era difícil predecir que muchas personas usaran este comportamiento. Pero probablemente debería ir a los documentos de migración (que necesitan algo de amor)

Sho '' nuff. Ésos son los únicos. Estaba buscando cambios relacionados con la transclusión, pero este es claramente un cambio en el alcance aislado que es incidental a la transclusión. ¡Gracias!

Este cambio también rompió nuestro código. Sería bueno tener una forma declarativa person de esas directivas y usarlas en el ámbito principal. Parece que es un caso de uso común después de ver cuántas personas lo estaban usando antes de que se corrigiera este bug en 1.2.18.

Aquí está la demostración que funciona en 1.2.17 y rota desde 1.2.18

http://plnkr.co/edit/QswOxN?p=preview

@evgenyneu : asumiendo que controlas tanto el contenido interno como la directiva del contenedor, hemos descubierto que usar ng-include en lugar de transclude es claro, fácil y nos da el patrón de herencia que queremos. Simplemente pasamos un nombre de plantilla como un atributo en la declaración de la directiva externa, y colocamos una directiva ng-include en el mismo lugar donde antes teníamos la directiva transclude. Problema resuelto.

Si desea que la plantilla de contenido interno esté en línea en la plantilla de vista, simplemente use ng-template para envolver la plantilla en la misma posición que estaba usando antes.

@xmlilley , excelente consejo, muchas gracias. De hecho, es el enfoque más limpio.

Aquí está la demostración: http://plnkr.co/edit/4MwEL3?p=preview

Chicos, saben que pueden hacer esto en su lugar correctamente http://plnkr.co/edit/fw7thti1u4F9ArxsuYkQ?p=preview --- no es perfecto, pero lo lleva allí

@caitp , bonito, gracias. ¡Tenemos muchas soluciones!

Para aquellos que ven 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;
      }
    }
  }
})

@Izhaki - FWIW, +10. No rompe las convenciones actuales, pero agrega una forma limpia y declarativa de acceder voluntariamente a un caso de uso común. ¡Gracias!

: +1: para la solución de @Izhaki . Usaré esto como está, pero me sentiría mucho más cómodo si se incluyera en angular.

+1

@Izhaki +1, ¡gran ejemplo!

Gracias por la directiva de inclusión personalizada. Terminé haciendo una conversión personalizada separada en lugar de anular la predeterminada. Hay un par de llamadas a funciones en su parche de las que no estoy seguro de dónde provienen. minErr () y startTag ()

@dehru : estas funciones son internas de AngularJS.

@Izhaki ¡muchas gracias! He bifurcado su plunk que repite el contenido transcluido, si hay alguien interesado.
Vale la pena mencionar que ng-transclude = "parent" podría no funcionar de la forma esperada.
http://plnkr.co/edit/S6ngqz?p=preview

plunker_ng-transclude_ng-repeat

@Izhaki Muy agradable.
Parece algo que debería estar en una solicitud de extracción a angular.
Al menos en una directiva separada con un repositorio de Github, ¿cree que puede publicarlo (para que pueda ofrecer algunos cambios ...)?
Gracias

Izhaki, esto es asombroso. Te quiero.

@Izhaki Esta es una muy buena solución al problema de los osciloscopios secundarios. Gracias.

Sin embargo, estoy confundido y me preguntaba si podría explicar cómo la herencia de la función $ transclude se traslada de la directiva externa a la directiva ngTransclude. No se indica explícitamente en ningún lugar oficial que pudiera encontrar, pero asumí que transclude: true tenía que usarse en la directiva para usar la función $ transclude en la función de enlace. Después de jugar un poco con el código, descubrí que usar transclude: true realmente rompe el código. ¿Que está pasando aqui?

Luché con esto durante días, así como con el hecho de que transcluir inserta los elementos transcluidos dentro del marcador de posición de transcluir en lugar de reemplazarlos con ellos.

¡Descubrí que ng-transclude-replace maneja ambos problemas! http://gogoout.github.io/angular-directives-utils/#/api/ng -directives-utils.transcludeReplace

¡No necesito propagar la directiva ng-repeat en la directiva repetida ni nada por el estilo! Todo parece estar funcionando, incluidas todas las validaciones / vinculaciones.

Aquí hay un fragmento de mi código:

<form-field label="Roles" required>
    <checkbox-group>
        <checkbox ng-repeat="role in roles" label="{{role.description}}">
            <input type="checkbox" name="selectedRoles" ng-model="role.selected" value="{{role.description}}" ng-required="!hasRole" />
        </checkbox>
    </checkbox-group>
</form-field>

@abobwhite Gracias por mencionar ng-transclude-replace, gran solución.

Recientemente me encontré con este problema. Si bien @Izhaki ha trabajado para mí, tengo curiosidad por saber si ha surgido una mejor práctica en el tiempo transcurrido desde esta discusión. En particular, ¿ha habido algún interés en hacer que ng-transclude="sibling | parent | child" forme parte del núcleo angular?

@telekid : no tenemos planes de incluir esta función en el núcleo en este momento.

Veo que hay soluciones y soluciones alternativas, pero sería muy conveniente si hubiera una forma de acceder al ámbito interno de la directiva.

Me gustaría señalar que actualicé el mod de transcluir que @Izhaki creó a angular 1.5 para que funcione con transclusión de múltiples ranuras.
Sucursal: https://github.com/NickBolles/ngTranscludeMod/tree/Angular1.5-multi-slot
PR: https://github.com/Izhaki/ngTranscludeMod/pull/2
Plunker: http://plnkr.co/edit/5XGBEX0muH9CSijMfWsH?p=preview

Lo que terminé haciendo fue usar $parent . Es el armario de la vainilla sin tener que añadir demasiadas cosas.

Entonces tengo algo como:

angular.module('test').directive('myDirectiveWithTransclusion', function() {
     return {
          restrict: 'E'
          transclude: {
               transcludeThis: 'transcludeThis'
          }
          template: "<div ng-repeat='item in array'><div ng-transclude='transcludeThis'></div></div>"
     }
})
<my-directive-with-transclusion>
     <transclude-this>
          {{$parent.item}}
     </transclude-this>
</my-directive-with-transclusion>

Hola @ moneytree-doug: utilizo esta solución que proporcionaste, pero encuentro el alcance de transclude: este sigue siendo el alcance de la directiva, no el hijo del nuevo alcance que genera ng-repeat. ¿Puedes darme algunas sugerencias?

@szetin ¿Podrías poner un jsfiddle que me muestre lo que esperas frente a lo que está sucediendo?

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