Angular.js: angulaire 1.2.18 : problème ng-repeat avec transclude

Créé le 17 juin 2014  ·  48Commentaires  ·  Source: angular/angular.js

Lorsque vous passez à une directive par ng-transclude, le contenu html avec la référence {{item}} que vous souhaitez répéter (via ng-repeat="item in collection" implémenté dans la directive) ne fonctionne pas avec la version 1.2.18

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

Commentaire le plus utile

Ce que j'ai fini par faire, c'est d'utiliser simplement $parent . C'est le placard à la vanille sans avoir à rajouter trop de choses.

J'ai donc quelque chose comme :

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>

Tous les 48 commentaires

oh seigneur. @petebacondarwin veut en faire un autre ? c'est vraiment la chose "ne pas créer une portée de frère avec ng-transclude" à nouveau, c'est juste que cela a fonctionné avant pour ce cas en raison de la rupture

Malheureusement, ce n'est pas ainsi que fonctionne la transclusion de ng-transclude . Ce que vous essayez de faire, c'est de créer une directive de conteneur qui utilise ses enfants comme "modèle" de ce qu'il faut supprimer dans votre propre directive.

J'ai rencontré cela à quelques reprises dans le passé et j'ai eu un long débat avec Misko à ce sujet. Le contenu qui est transclus est par définition lié à la portée du lieu où la directive est instanciée ; pas au champ d'application du modèle de la directive.

Auparavant, cela pouvait avoir fonctionné car nous liions en fait la transclusion à la mauvaise portée dans certains cas impliquant des scénarios de transclusion imbriqués.

Donc, en fait, vous n'avez pas vraiment besoin d'utiliser la transclusion ici, car ce que vous essayez vraiment de faire est simplement d'injecter le code HTML interne dans votre propre modèle. Vous pouvez le faire dans la fonction de compilation comme ceci :

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

Un autre exemple de ceci (je crois):

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>',
    }
});

A fonctionné avec Angular 1.2.1, mais pas avec 1.2.18;

Le développeur innocent ne pouvait que s'attendre à ce que le code ci-dessus fonctionne. Ce doc dit :

...parfois, il est souhaitable de pouvoir transmettre un modèle entier plutôt qu'une chaîne ou un objet. Disons que nous voulons créer un composant "boîte de dialogue". La boîte de dialogue doit pouvoir envelopper tout contenu arbitraire.

Alors que la documentation de ngTransclude dit :

Directive qui marque le point d'insertion du DOM transclus de la directive parente la plus proche qui utilise la transclusion. Tout contenu existant de l'élément sur lequel cette directive est placée sera supprimé avant que le contenu transclus ne soit inséré.

En quoi cela diffère- t-il de la définition de

... créer une directive de conteneur qui utilise ses enfants comme "modèle" de ce qu'il faut supprimer dans votre propre directive

Je ne comprends vraiment pas pourquoi la transclusion n'est pas la bonne solution ici. Si quoi que ce soit, je m'attendrais à ce que la solution raisonnable implique l'injection de la portée du modèle à la fonction de transclusion.

La différence est que le contenu transclus est lié à "l'extérieur", c'est-à-dire la portée de l'endroit où se trouve l'élément <people> .
Alors que ce que vous voulez, c'est que le modèle en ligne soit lié à "l'intérieur", c'est-à-dire au champ d'application de la directive.
Si votre directive ne crée pas sa propre portée, c'est à peu près la même chose. Si votre directive crée une portée isolée, par exemple, ce n'est certainement pas la même chose. La portée interne n'a pas accès à la portée externe.

Je suppose que vous pourriez créer votre propre directive qui injecte effectivement la portée du modèle dans la fonction de transclusion ...

Ce que vous dites est parfaitement logique - mais ce n'est que si vous comprenez les profondeurs d'Angular. Mon point est que l'exemple ci-dessus devrait fonctionner d'une manière ou d'une autre sans trop de travail supplémentaire.

Cela me semble très raisonnable et très pratique pour que la fonction de transclusion puisse accéder au modèle (ou à la portée "intérieur") d'une manière ou d'une autre. Je peux penser à de nombreux cas pour lesquels cela sera nécessaire.

Le même problème est expliqué dans ce blog . Je pense que de plus en plus de gens se plaindront de la situation actuelle (il y a déjà une multitude de problèmes liés sur Github à cause de ce comportement).

Et merci beaucoup pour le code. Je le reproduis ici pour le bénéfice des autres :

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

:-) Je suis d'accord que la transclusion n'est pas un sujet facile à comprendre sans beaucoup de grattage de tête et de claquement de clavier.
Peut-être devons-nous clarifier davantage la documentation sur le fait que ng-transclude liera le contenu transclus à la portée « extérieure » ?

Personnellement, je pense que la documentation actuelle, au moins sur cette page pivot , est assez explicite et claire :

À quoi sert cette option de transclusion, exactement ? transclude permet au contenu d'une directive avec cette option d'accéder à la portée en dehors de la directive plutôt qu'à l'intérieur.

(et tout ce qui suit l'illustre davantage).

J'envisagerais d'ajouter peut-être la directive que vous avez fournie au cadre, en la qualifiant éventuellement de « ng-transclude-internal » ? Je connais au moins une autre personne qui s'y est essayée, avec la directive appelée "transcope".

J'ai essayé une solution, mais j'ai été confronté à un autre problème, pourquoi dans la portée parent ng-repeat n'est pas la portée de la directive

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

@luboid la raison pour laquelle cela ne fonctionne pas dans votre plunker est que votre directive a une portée isolée et que le DOM compilé modifié (ne le faites pas, d'ailleurs, c'est une façon idiote de résoudre ce problème) utilisera un frère du parent de la portée d'isolat.

J'ajouterai un exemple d'une façon appropriée de faire en sorte que cela fonctionne comme vous l'attendez. (Mais, c'est toujours un design assez horrible en général, il n'y a aucune bonne raison de le faire)

En fait, à bien y penser, avec ng-repeat ou d'autres directives de transclusion d'éléments, vous ne pouvez pas vraiment résoudre ce problème. Alors oui, ça ne fonctionnera plus depuis la version 1.2.0

@petebacondarwin Une question vraiment novice ici. Pourquoi utiliser var innerScope au lieu de simplement :

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

Il est généralement préférable de créer une nouvelle portée si vous allez compiler de nouveaux éléments, car vous n'êtes pas sûr si la directive d'origine est colocalisée avec une directive complexe qui veut également créer une portée, etc. Mais vous pourrez peut-être obtenir loin avec ça ici...

Merci pour l'aide,
J'utiliserai des modèles externes ($templateCache), puis les choses vont un peu simples, le modèle de base est compilé avec la portée de la directive

@Izhaki @petebacondarwin Merci beaucoup pour cette directive d'inclusion. Je viens enfin de passer de 1.2.16 à 1.2.20 et il a fallu un peu de temps pour comprendre pourquoi mon application a commencé à se briser si fort.

Je pensais que la transclusion était un concept assez simple avant de trouver ce fil. Nan.

Merci à tous ceux qui ont aidé à éclaircir cela. La mise à jour vers 1.2.21 a cassé une grande partie de notre interface à cause des problèmes décrits ici, et maintenant nous sommes sur la bonne voie.

@caitp @petebacondarwin : Ce serait très utile si nous pouvions obtenir deux informations supplémentaires :

  1. Quel changement décisif qui s'est produit quelque part "à propos de la version 1.2.0" (comme l'a dit Caitlin) a fait exploser soudainement cette approche ? Je comprends ce qui ne fonctionne plus, mais je ne vois rien de spécifique dans le journal des modifications près de cette version qui semble être en corrélation avec ce problème particulier.
  2. Quelle est l'approche alternative que les utilisateurs finaux devraient utiliser à la place de ng-transclude si nous voulons des conteneurs isolés et réutilisables à partir desquels un contenu interne arbitraire peut hériter ? Exemple : une grande application multi-locataires où même l'interface est variable et totalement axée sur les données. Donc, nous avons des choses comme des contrôles de liste qui doivent aller récupérer des données pour se remplir, puis avoir une interaction/un comportement cohérent. Nous aimerions pouvoir standardiser une directive wrapper qui fournit ces choses. Mais les modèles que nous utilisons pour les éléments de liste (qui peuvent contenir des directives internes) varient en fonction de la situation. Et les listes sont souvent imbriquées, de manière récursive, en fonction de l'arborescence des données qui les génèrent. Nous avons donc besoin d'isoler ces listes imbriquées. Nous contrôlons tout le code, nous n'utilisons donc pas la transclusion pour isoler l'intérieur de l'extérieur. Nous recherchons plutôt une approche agréable et déclarative des conteneurs et de leur contenu arbitraire, comme cela est explicitement recommandé dans le Guide de la directive. ng-include est-il la meilleure option pour cela maintenant (en passant le chemin d'accès au modèle interne en tant qu'attribut sur la déclaration du conteneur), même s'il nous éloigne des modèles en ligne et semble donc un peu moins idiomatique?

Je comprends ce qui ne fonctionne plus, mais je ne vois rien de spécifique dans le journal des modifications près de cette version qui semble être en corrélation avec ce problème particulier.

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

je faisais référence à ceux-ci en particulier

  • transmettre uniquement la portée d'isolat aux enfants qui appartiennent à la directive d'isolat (d0efd5ee)
  • faire isoler la portée vraiment isoler (909cabd3, #1924, #2500)

Mais, je ne me souviens même plus vraiment de quoi je parlais, qui sait ヽ༼ຈل͜ຈ༽ノ

Dans le changelog, il n'y a que deux types de messages : les changements et les changements de rupture. Cela n'a pas été considéré comme un changement décisif, mais plutôt comme une correction de bogue. Il était difficile de prédire que de nombreuses personnes utilisaient ce comportement. Mais cela devrait probablement aller dans les documents de migration (qui ont besoin d'un peu d'amour)

Sho' 'nuff. Ce sont ceux-là. Je cherchais des changements liés à la transclusion, mais il s'agit clairement d'un changement dans la portée de l'isolat qui est accessoire à la transclusion. Merci!

Ce changement a également cassé notre code. Ce serait bien d'avoir un moyen déclaratif simple d'accéder aux variables person de ces directives et de les utiliser dans la portée parent. On dirait que c'est un cas d'utilisation courant après avoir vu combien de personnes l'utilisaient avant que ce bug soit corrigé en 1.2.18.

Voici la démo qui fonctionne en 1.2.17 et cassée depuis 1.2.18

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

@evgenyneu : En supposant que vous

Si vous souhaitez que le modèle de contenu interne soit en ligne sur le modèle de vue, utilisez simplement ng-template pour envelopper le modèle dans la même position que vous utilisiez auparavant.

@xmlilley , excellent conseil, merci beaucoup. C'est en effet l'approche la plus propre.

Voici la démo : http://plnkr.co/edit/4MwEL3?p=preview

Les gars, vous savez que vous pouvez le faire à la place correctement http://plnkr.co/edit/fw7thti1u4F9ArxsuYkQ?p=preview --- ce n'est pas parfait, mais cela vous y amène

@caitp , gentil, merci. Nous avons beaucoup de solutions !

Pour ceux qui regardent ce numéro, j'ai créé une directive « améliorée » ng-transclude, dont la valeur d'attribut définit la portée interne recherchée et peut être l'une des 3 :

  • silbing - La portée du contenu transclus est sœur de l'élément où la transclusion se produit. C'est le comportement actuel de ng-transclude.
  • parent - La portée du contenu transclus est celle de l'élément où la transclusion se produit.
  • child - La portée du contenu transclus est la portée enfant de la portée de l'élément où la transclusion se produit.

Exemple d'utilisation :

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

Exemple complet

Voir ce plunk pour un exemple de tout.

La sortie ressemble à ceci :

image

La source

.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. N'enfreint pas les conventions actuelles, mais ajoute une manière propre et déclarative d'accéder volontairement à un cas d'utilisation commun. Merci!

:+1: pour la solution de @Izhaki . Je l'utiliserai tel quel, mais je me sentirais beaucoup plus à l'aise s'il était inclus dans angulaire.

+1

@Izhaki +1, excellent exemple !

Merci pour la directive transclude personnalisée. J'ai fini par créer une transclusion personnalisée distincte au lieu de remplacer la valeur par défaut. Il y a quelques appels de fonction dans votre patch dont je ne sais pas d'où ils viennent. minErr() et startTag()

@dehru - ces fonctions sont internes à AngularJS.

@Izhaki merci beaucoup ! J'ai fourchu votre plunk qui répète le contenu transclus, si quelqu'un est intéressé.
Il convient de mentionner que ng-transclude="parent" pourrait ne pas fonctionner comme vous le souhaiteriez.
http://plnkr.co/edit/S6ngqz?p=preview

plunker_ng-transclude_ng-repeat

@Izhaki Très sympa.
On dirait quelque chose qui devrait être dans une pull request à angulaire ..
Au moins dans une directive séparée avec un dépôt Github, pensez-vous que vous pouvez le publier (afin que je puisse proposer quelques modifications....) ?
Merci

Izhaki, c'est génial. Je vous _coeur_.

@Izhaki C'est une très bonne solution au problème des portées enfants. Merci.

Je suis cependant confus et je me demandais si vous pouviez expliquer comment l'héritage de la fonction $ transclude est transféré de la directive externe à la directive ngTransclude? Ce n'est explicitement indiqué nulle part où j'ai pu trouver, mais j'ai supposé que transclude: true devait être utilisé sur la directive pour utiliser la fonction $transclude dans la fonction de lien. Après avoir joué un peu avec le code, j'ai découvert que l'utilisation de transclude: true casse en fait le code. Que se passe t-il ici?

Je me suis battu contre cela pendant des jours ainsi que contre le fait que transclude insère les éléments transclus dans l'espace réservé transclude au lieu de le remplacer par eux.

J'ai trouvé que ng-transclude-replace gère les deux problèmes ! http://gogoout.github.io/angular-directives-utils/#/api/ng -directives-utils.transcludeReplace

Je n'ai pas besoin de propager la directive ng-repeat dans la directive répétée elle-même ou quoi que ce soit du genre ! Tout semble fonctionner, y compris toutes les validations/liaisons.

Voici un extrait de mon code :

<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 Merci d'avoir mentionné ng-transclude-replace , excellent correctif.

J'ai récemment rencontré ce problème. Alors que @Izhaki a travaillé pour moi, je suis curieux de savoir si une meilleure pratique a émergé depuis cette discussion. En particulier, y a-t-il eu un intérêt à intégrer ng-transclude="sibling | parent | child" dans le noyau angulaire ?

@telekid - Nous n'avons pas l'intention d'inclure cette fonctionnalité dans le noyau pour le moment.

Je vois qu'il existe des solutions et des contournements, mais ce serait très pratique s'il y avait un moyen d'accéder au champ d'application de la directive.

Je tiens à souligner que j'ai mis à jour le mod de transclude que @Izhaki a créé à angulaire 1.5 afin qu'il fonctionne avec la transclusion à plusieurs emplacements.
Branche : 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

Ce que j'ai fini par faire, c'est d'utiliser simplement $parent . C'est le placard à la vanille sans avoir à rajouter trop de choses.

J'ai donc quelque chose comme :

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>

Salut @moneytree-doug : j'utilise cette solution que vous avez fournie, mais je trouve que la portée de transclude-c'est toujours la portée de la directive, pas l'enfant de la nouvelle portée qui est générée par ng-repeat. Peut-tu me faire des suggestions?

@szetin Pourriez-vous mettre en place un jsfiddle me montrant ce que vous attendez par rapport à ce qui se passe ?

Cette page vous a été utile?
0 / 5 - 0 notes