Angular.js: ng-transclude ne doit pas créer de nouvelle portée sœur.

Créé le 20 déc. 2013  ·  69Commentaires  ·  Source: angular/angular.js

Il s'agit davantage d'une demande de changement et j'aimerais voir ce que les autres pensent.

À mon humble avis, ng-transclude ne devrait pas créer sa propre portée ou au moins avoir un moyen de l'empêcher de le faire. La raison en est qu’une directive qui demande la transclusion a déjà les moyens de spécifier si elle veut ou non avoir un champ d’application ou un champ d’application isolé ou pas du tout. Il utilise la directive ng-transclude pour marquer l'endroit où il souhaite insérer le contenu. Lorsque ng-transclude crée son propre champ d'application frère, cela brise en quelque sorte les attentes de la directive qui définit le type de champ d'application qu'il veut et il se produit la manifestation de la confusion populaire «valeur» contre «objet.valeur».

Voici un exemple de cas où une nouvelle portée n'a pas de sens à mon avis:

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

Tout ce que cette directive veut, c'est remplacer le <box>content</box> par un <div>content</div> et le contenu doit avoir la portée isolée.

La création d'une structure imbriquée de directives comme celle-ci conduit à une pollution de l'arborescence des portées. Voici un exemple de plunker (http://plnkr.co/edit/DwukVGGprFFjQuVY8yTz) de trois directives imbriquées qui créent une arborescence de portée comme celle-ci:

< 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

Ce comportement ne semble pas ajouter de valeur à son objectif mais crée beaucoup de confusion chez les débutants.

Pour le moment, j'utilise la solution de contournement suivante qui réalise exactement ce que fait l'exemple précédent:

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

Voici un exemple de plunker (http://plnkr.co/edit/46v6IBLkhS71L1WbUDFl) qui illustre ce concept. Cela laisse l'arbre de la portée bien rangé:

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

Et la liaison bidirectionnelle fonctionne comme beaucoup l'attendent lorsqu'ils lient «valeur» plutôt que «object.value». (Je pense que le fait de ne passer que la "valeur" fonctionne dans certains cas, mais pas l'autre et de blâmer la nature de l'héritage prototypique en javascript n'est pas une bonne excuse. Le fait que de nombreuses personnes trouvent ce comportement inattendu indique qu'il y a un défaut architectural .)

J'adorerais entendre ce que les autres pensent et utiliser des cas où ils pensent qu'il est logique de créer une nouvelle portée fraternelle pour ng-transclude.

Lots of comments $compile high won't fix bug

Commentaire le plus utile

Heureusement que je suis passé à Ember il y a des années. :)

Tous les 69 commentaires

Où transclude crée-t-il un nouveau périmètre? http://plnkr.co/edit/EuHaBR26JgAegQKvwOGH?p=preview Je ne le vois pas

Je parle de la directive ng-transclude. Ce que vous avez dans votre exemple est exactement ce que fait mon travail.

c'est une demande valide. nous envisagions cela pour la version 1.2 mais cela se rapprochait de la version finale et nous ne voulions pas introduire ce changement de rupture.

nous devrions le considérer pour 1.3

Joli! Je suis content que vous y ayez déjà réfléchi.

+1 pour cela. Je pense que mon problème est lié à ceci: j'utilise ng-transclude dans une directive avec des formulaires et je dois passer par la portée. $$ childHead pour accéder à l'objet de validation de formulaire mais je n'ai pas de problème pour accéder à mes modèles.

Voici un exemple: http://fiddle.jshell.net/39cgW/3/

+1 rencontre ce problème aujourd'hui et je déteste jeter des $parent partout.

Donc, pour trouver une solution à cela, il semble qu'il y ait deux possibilités

1) modifiez la directive ngTransclude pour spécifier sa portée (elle pourrait en fait être réduite beaucoup plus que cela, je pense - aucun contrôleur n'est nécessaire)

ou alors

2) Ne créez pas de nouvelle portée lorsque la portée n'est pas spécifiée

Donc, nous pourrions économiser quelques octets avec l'option 1) et c'est bien, 2) serait la plus petite solution (suppression de 3 lignes environ) et il n'est pas clair pour moi qu'il existe des cas d'utilisation où la création d'une nouvelle portée a implicitement un sens ( peut-être qu'il y en a, mais cela semble complètement contraire à la façon dont la transclusion est décrite dans la documentation)


Ou, si vous vouliez être super sophistiqué, peut-être pourriez-vous éviter complètement d'interrompre les modifications en permettant à ngTransclude de spécifier qu'il souhaite une nouvelle portée ou non, via la valeur d'attribut.

Pensées?

Je ne comprends pas la différence entre votre 1 et votre 2!

Juste mon avis, mais je pense que la rétrocompatibilité sera importante pour ce changement. J'imagine qu'il existe de nombreuses applications (la mienne incluse) qui ont résolu ce problème de portée en utilisant des éléments tels que $ parent et scope. $$ childHead. Toute mise à jour qui modifie ce comportement causera des maux de tête (mais il vaut peut-être mieux causer des maux de tête le plus tôt possible).

Cela dit, d'un point de vue théorique, je pense qu'il est plus logique que ng-tranclude ait la même portée que la directive par défaut. L'intérêt de la transclusion de contenu est que vous voulez qu'il fasse partie intégrante du contenu. Il y a des moments où j'ai beaucoup d'imbrication de directives avec des transcludes, mais je veux toujours qu'elles se comportent comme un grand composant. Les avoir avec toutes les portées différentes rend cela très délicat.

Tout juste mes pensées. À tout le moins, le simple fait d'avoir l'option sera un cran au-dessus de la situation actuelle. :)

@troch pour clarifier, nous injectons ce qui suit dans le contrôleur de ngTranscludeDirective:

        // 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 directive appelle cette fonction sans portée, et en tant que telle, la portée est indéfinie ... Puis dans boundTranscludeFn , elle crée une nouvelle portée si le transcludeScope est faux ...

Donc, ce que je dis, c'est que pour 1), nous pouvons simplement spécifier la portée actuelle de la fonction transclude (puisque cette directive est une directive voisine de tout ce qui pourrait avoir une portée isolée, cela devrait toujours nous donner la portée d'origine) .

Alternativement, 2), ne créez pas de nouvelle portée et utilisez simplement par défaut la portée actuelle (dans createBoundTranscludeFn) (changement potentiellement cassant, et potentiellement cassant beaucoup de tests).

Les deux sont assez simples à faire.

+1

+1

+1

Certainement +1

+1

+1

+1

+1 s'il vous plaît

Vous savez ce qui serait plutôt cool, bien que peut-être pas totalement faisable à temps pour la 1.3, les proxys ES6 pourraient rendre la transclusion vraiment sympa --- suivre les propriétés de la portée de la transclusion, mais avoir la bonne position dans la hiérarchie.

Si l'implémentation Proxy n'est pas disponible, elle pourrait probablement revenir à scope. $ New (), donc il serait peut-être possible de faire ce travail assez tôt. La chose la plus délicate est que la spécification est un peu bancale.

Ainsi, vous obtiendrez toujours des portées indésirables si vous souhaitez que la hiérarchie des portées soit très propre, mais au moins vous auriez l'effet secondaire de ne pas rompre de manière inattendue les liaisons de données. Je ne sais pas.

+1

+1

+1

+1

+1

@caitp

Ou, si vous vouliez être super sophistiqué, peut-être pourriez-vous éviter complètement d'interrompre les modifications en permettant à ngTransclude de spécifier qu'il souhaite une nouvelle portée ou non, via la valeur d'attribut.

Tout ce qui n'a pas de changements de rupture obtient mon vote.

+1

+1

+1

+1

+1

+1. Je me suis toujours posé des questions sur ce comportement. J'aime cette solution de caitp:

@caitp

Ou, si vous vouliez être super sophistiqué, vous pourriez peut-être éviter de casser complètement les changements en permettant à> ngTransclude de spécifier qu'il veut ou non une nouvelle portée, via la valeur d'attribut.

+1

+1

+1

Pour ceux qui regardent ce problème, j'ai créé une directive ng-transclude `` améliorée '', 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 un frère de l'élément où se produit la transclusion. 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;
      }
    }
  }
})

Comme le problème que j'ai soulevé plus tôt dans # 8609 a été fermé en tant que duplicata de ce fil, je le répète ici.

À mon avis, la manière actuelle de créer une portée pour la partie transclue du DOM est très illogique!
Cela va à l'encontre du flux normal en angulaire, et devrait être corrigé!

Voici un extrait de mon numéro précédent:

J'ai créé un petit plunk pour illustrer mon problème ici.

le contrevenant principal est dans cette directive

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

Lorsqu'un utilisateur fait quelque chose comme ceci:

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


Le résultat sera surprenant pour beaucoup, en particulier pour les nouveaux utilisateurs. Et c'est même un cas d'utilisation (trop) simplifié. Essayez de mettre dans l'affichage de certains messages de validation là-bas;)

C'est un cas d'utilisation différent de celui où ce problème a commencé, mais je conviens qu'il s'agit essentiellement du même problème!

Il est beaucoup plus logique de transcender la portée avec l'élément dom à la place. Si une nouvelle portée est vraiment nécessaire, un utilisateur peut quand même mettre un ngController sur l'élément ngTransclude . Ou il peut y avoir un indicateur facultatif sur ngTransclude qui en déclenche un. Cet indicateur peut également être utilisé pour résoudre le cas d'utilisation # 5489. Et il peut également être utilisé pour la solution proposée par caitp.

+1

+1

+1

+1

+1

+1

+1

+1

+1

+1

+1

+1

+1 semble que la directive transclue crée un champ d'application même lorsqu'on lui demande de ne pas le faire. exemple http://plnkr.co/edit/Wn81IBkE87vtigXvjmIa?p=preview

+1

+1

+1

+1

+1

+1

+1

+1

+ 1 - existe-t-il des solutions de contournement valides pour cela?

@nikkwong , il y a quelques solutions de contournement publiées dans ce fil. Je sais que j'ai fait un lien dans un plunk qui a montré une solution de contournement.

OK, donc même si cela arrive, ce ne sera pas avant la 1.5.x

Mais avant cela, j'ai une inquiétude. La principale raison de la création d'une nouvelle étendue (appelez-la un frère de l'étendue isolate, ou plus précisément un enfant de l'étendue d'origine d'où provient le contenu transclus) est d'éviter les fuites de mémoire.

Comparez ce plunker:
http://plnkr.co/edit/3NVxdYGy1AFDvD0M2BYI?p=preview

avec une version qui réutilise la portée d'où provient la transclusion d'origine:
http://plnkr.co/edit/MXFz2awcqwQQ7R882Xwz?p=preview

Dans la deuxième version, lorsque vous activez et désactivez le contenu transclus, le nombre d'observateurs continue d'augmenter - une fuite de mémoire.

@petebacondarwin cela devrait certainement être une nouvelle portée afin qu'il puisse être détruit avec ses observateurs. Je pense que la plupart des problèmes liés à la transclusion sont dus au fait que ng-transclude créerait une portée sœur de la portée contenante, ce qui rendrait impossible l'accès à ses variables via l'héritage prototypique. Ou ai-je tort?

Nous avons réalisé que le problème est la façon dont la liaison angulaire à 2 voies fonctionne, définitivement ng-transclude crée une portée enfant, mais il y aura de nombreuses occasions où vous vous retrouverez dans la même situation, par exemple ng-repeat crée une portée enfant. Après avoir examiné le code angulaire, nous avons réalisé que le problème de la liaison bidirectionnelle ne fonctionnait pas avec l'attribut object, mais fonctionnait bien avec les objets eux-mêmes

Problème: http://plnkr.co/edit/Wn81IBkE87vtigXvjmIa?p=preview

Solution: http://plnkr.co/edit/KShClgQVwIjscXPzVRwR?p=preview

Ce n'est pas parfait, mais sachant que cela a résolu beaucoup de nos problèmes, ne faites jamais de liaison bidirectionnelle sur un attribut

Notez que cette solution est déjà suggérée par mbykovskyy, quand il a soulevé un problème, je donne juste un exemple car il m'a fallu un certain temps pour le comprendre.

@petebacondarwin Ce ne sera qu'une fuite temporaire, jusqu'à ce que la portée de détention soit détruite. Voici (un plunk) [http://plnkr.co/edit/Q587WQnX0u0u7JjhtCxa?p=preview] qui montre que si vous désactivez le transclude-holding-scope, tout sera libéré très bien.
C'est toujours un point qui mérite attention. Peut-être qu'un grand Waring dans la documentation sur cette fuite possible pourrait suffire à le réparer?

Eh bien, toute fuite JS n'est que temporaire jusqu'à ce que vous actualisiez le navigateur ;-)
Le 8 septembre 2015 à 16h41, "Sander Elias" [email protected] a écrit:

@petebacondarwin https://github.com/petebacondarwin Ce ne sera qu'un
fuite temporaire, jusqu'à ce que la lunette de maintien soit détruite. Voici une
plunk) [http://plnkr.co/edit/Q587WQnX0u0u7JjhtCxa?p=preview] qui montre
que si vous désactivez le transclude-holding-scope, tout sera
sorti très bien.
C'est toujours un point qui mérite attention. Peut-être un grand Waring dans le
des documents sur cette fuite éventuelle pourraient suffire à le réparer?

-
Répondez directement à cet e-mail ou affichez-le sur GitHub
https://github.com/angular/angular.js/issues/5489#issuecomment -138603298
.

@SanderElias Les utilisateurs de notre application Angular sont occupés du début à la fin de la journée.
On voit déjà que l'utilisation de la mémoire augmente au cours de la journée et il faut être très prudent dans ce qu'on met sur la page, introduire plus de fuites éventuelles est risqué.

@troch - transclusion crée en fait une portée enfant de la portée où se trouve à l'origine le contenu transclus. Cela a été cassé il y a quelques versions (définitivement en 1.2) où à la place, il a simplement créé un enfant du parent de la portée des directives actuelles. Cela signifiait que les transclusions profondément imbriquées avaient en fait la mauvaise portée de transclusion.

Je suis d'accord avec @petebacondarwin , et même lorsque le comportement actuel n'est pas 100% intuitif, le comportement actuel fonctionne mieux pour éviter toute fuite. Je suis enclin à fermer ce problème car Wont Fix

Heureusement que je suis passé à Ember il y a des années. :)

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