Angular.js: angular 1.2.18: problema de repetição de ng com transcluir

Criado em 17 jun. 2014  ·  48Comentários  ·  Fonte: angular/angular.js

Ao passar para uma diretiva por ng-transclude, o conteúdo html com a referência {{item}} que você deseja repetir (por meio de ng-repeat = "item na coleção" implementado na diretiva) não funciona com a versão 1.2.18

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

Comentários muito úteis

O que acabei fazendo foi apenas usar $parent . É o armário da baunilha sem ter que acrescentar muitas coisas.

Então, eu tenho 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 comentários

oh legal. @petebacondarwin quer fazer mais um desses? este é realmente o "não crie um escopo irmão com ng-transclude" de novo, só que funcionou antes para este caso devido a falhas

Infelizmente, não é assim que a transclusão ng-transclude funciona. O que você está tentando fazer é criar uma diretiva de contêiner que faz uso de seus filhos como um "modelo" do que deve ser eliminado dentro de sua própria diretiva.

Já me deparei com isso algumas vezes no passado e tive um longo debate com Misko sobre isso. O conteúdo transcluído está, por definição, vinculado ao âmbito do local onde a diretiva é instanciada; não ao âmbito do modelo da diretiva.

Anteriormente, isso pode ter funcionado, pois estávamos vinculando a transclusão ao escopo errado em alguns casos que envolviam cenários de transclusão aninhada.

Então, na verdade, você realmente não precisa usar a transclusão aqui, pois o que você realmente está tentando fazer é simplesmente injetar o HTML interno em seu próprio modelo. Você pode fazer isso na função de compilação desta forma:

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

Outro exemplo disso (acredito):

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

Funcionou com Angular 1.2.1, mas não com 1.2.18;

O desenvolvedor inocente só poderia esperar que o código acima funcionasse. Este documento diz:

... às vezes é desejável ser capaz de passar um template inteiro ao invés de uma string ou um objeto. Digamos que desejamos criar um componente de "caixa de diálogo". A caixa de diálogo deve ser capaz de envolver qualquer conteúdo arbitrário.

Enquanto a documentação do

Diretiva que marca o ponto de inserção para o DOM transcluído da diretiva pai mais próxima que usa a transclusão. Qualquer conteúdo existente do elemento em que esta diretiva é colocada será removido antes que o conteúdo transcluído seja inserido.

Como isso difere da definição de @petebacondarwin :

... cria uma diretiva de contêiner que faz uso de seus filhos como um "modelo" do que deve ser eliminado dentro de sua própria diretiva

Eu realmente não entendo por que a transclusão não é a solução certa aqui. Se qualquer coisa, eu esperaria que a solução razoável envolvesse injetar o escopo do template para a função de transclusão.

A diferença é que o conteúdo transcluído está vinculado ao "exterior", ou seja, o escopo do lugar onde o elemento <people> é encontrado.
Ao passo que o que você deseja é que o modelo embutido seja vinculado ao "interior", ou seja, ao escopo da diretiva.
Se a sua diretiva não cria seu próprio escopo, isso é aproximadamente a mesma coisa. Se sua diretiva cria um escopo isolado, digamos, então definitivamente não é a mesma coisa. O escopo interno não tem acesso ao escopo externo.

Eu acho que você poderia criar sua própria diretiva que realmente injeta o escopo do template na função de transclusão ...

O que você está dizendo faz todo o sentido - mas apenas se você compreender as profundezas do Angular. Meu ponto é que o exemplo acima deve funcionar de alguma forma sem muito trabalho extra.

Parece-me muito razoável e altamente prático para a função de transclusão ser capaz de acessar o escopo do modelo (ou 'dentro') de alguma forma. Posso pensar em muitos casos em que isso será necessário.

O mesmo problema é explicado neste blog . Minha estimativa é que mais e mais pessoas reclamarão sobre como as coisas estão no momento (já existe uma infinidade de questões relacionadas no Github como resultado desse comportamento).

E muito obrigado pelo código. Estou replicando aqui para o benefício de outros:

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

:-) Concordo que a transclusão não é um tópico fácil de entender sem muito arranhar a cabeça e bater no teclado.
Talvez seja necessário esclarecer ainda mais a documentação sobre o fato de que ng-transclude vinculará o conteúdo transcluído ao escopo "externo".

Pessoalmente, acho que a documentação atual, pelo menos nesta página fundamental , é bastante explícita e clara:

O que essa opção de transcluir faz, exatamente? transclude faz com que o conteúdo de uma diretiva com esta opção tenha acesso ao escopo fora da diretiva em vez de dentro dela.

(e tudo o que se segue ilustra isso ainda mais).

Eu consideraria adicionar talvez a diretiva que você forneceu à estrutura, possivelmente marcando-a como 'ng-transclude-internal'. Eu conheço pelo menos mais uma pessoa que tentou isso, com a diretiva chamada 'transcope'.

Eu tentei uma solução, mas me deparei com outro problema, por que em ng-repeat o escopo pai não é o escopo da diretiva

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

@luboid a razão que não funciona em seu plunker é porque sua diretiva tem um escopo isolado, e o DOM compilado modificado (não faça isso, a propósito, esta é uma maneira boba de resolver este problema) usará um irmão do pai do escopo isolado.

Acrescentarei um exemplo de uma maneira adequada de fazer isso funcionar como você espera. (Mas, este ainda é um design bastante horrível em geral, não há um bom motivo para fazer isso)

Na verdade, pensando bem, com ng-repeat ou outras diretivas de transclusão de elemento, você não pode realmente consertar isso. Então, sim, isso não funcionará desde a versão 1.2.0

@petebacondarwin Uma pergunta realmente novata aqui. Por que usar var innerScope vez de apenas:

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

Geralmente, é melhor criar um novo escopo se você for compilar alguns novos elementos, uma vez que você não tem certeza se a diretiva original está colocada com alguma diretiva complexa que também deseja criar um escopo, etc. Mas você pode conseguir longe com isso aqui ...

Obrigado pela ajuda,
Vou usar modelos externos ($ templateCache), então as coisas ficam um pouco simples, o modelo base é compilado com escopo de diretiva

@Izhaki @petebacondarwin Muito obrigado por essa diretiva de inclusão. Finalmente atualizei de 1.2.16 para 1.2.20 e demorou um pouco para ver por que meu aplicativo começou a quebrar tanto.

Eu pensei que a transclusão era um conceito muito simples antes de encontrar este tópico. Não.

Obrigado a todos que ajudaram a esclarecer isso. Atualizar para 1.2.21 quebrou muito de nossa interface por causa dos problemas descritos aqui, e agora estamos no caminho certo.

@caitp @petebacondarwin : Seria muito útil se pudéssemos obter duas informações adicionais:

  1. Qual mudança significativa que aconteceu em algum lugar "sobre a versão 1.2.0" (como Caitlin disse) resultou na explosão repentina dessa abordagem? Eu entendo o que não funciona mais, mas não vejo nada específico no changelog próximo a esse lançamento que pareça estar relacionado a este problema em particular.
  2. Qual é a abordagem alternativa que os usuários finais devem empregar em vez de ng-transcluir se quisermos contêineres isolados e reutilizáveis ​​dos quais o conteúdo interno arbitrário pode herdar? Exemplo: um aplicativo grande e multilocatário em que até mesmo a interface é variável e totalmente orientada a dados. Portanto, temos coisas como controles de lista que precisam recuperar dados para se popularizarem e, em seguida, têm interação / comportamento consistente. Gostaríamos de padronizar uma diretiva de invólucro que forneça essas coisas. Mas os modelos que usamos para os itens da lista (que podem conter diretivas internas) variam dependendo da situação. E as listas são freqüentemente aninhadas, recursivamente, com base na estrutura de árvore dos dados que as geram. Portanto, precisamos de isolamento para essas listas aninhadas. Nós controlamos todo o código, então não estamos usando a transclusão para isolar o interno do externo. Em vez disso, estamos apenas atrás de uma abordagem agradável e declarativa para os contêineres e seu conteúdo arbitrário, conforme explicitamente recomendado no Guia de Diretrizes. O ng-include é a melhor opção para isso agora (passar o caminho para o template interno como um atributo na declaração do contêiner), mesmo que nos afaste dos templates embutidos e, portanto, pareça um pouco menos idiomático?

Eu entendo o que não funciona mais, mas não vejo nada específico no changelog próximo a esse lançamento que pareça estar relacionado a este problema em particular.

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

Eu estava me referindo a eles em particular

  • apenas passe o escopo isolate para filhos que pertençam à diretiva isolate (d0efd5ee)
  • tornar o escopo isolado verdadeiramente isolado (909cabd3, # 1924, # 2500)

Mas não consigo mais me lembrar do que estava falando, quem sabe ヽ ༼ ຈ ل͜ ຈ ༽ ノ

No changelog, existem apenas dois tipos de mensagens: alterações e alterações importantes. Isso não foi considerado uma alteração significativa, mas sim uma correção de bug. Era difícil prever que muitas pessoas usaram esse comportamento. Mas provavelmente deve ir para os documentos de migração (que precisam de um pouco de atenção)

Sho '' nuff. Esses são os únicos. Eu estava procurando por mudanças relacionadas à transclusão, mas esta é claramente uma mudança no escopo isolado que é acidental à transclusão. Obrigado!

Essa mudança quebrou nosso código também. Seria bom ter uma maneira declarativa fácil de acessar as variáveis person dessas diretivas e usá-las no escopo pai. Parece que é um caso de uso comum depois de ver quantas pessoas o estavam usando antes que bug fosse corrigido em 1.2.18.

Aqui está a demonstração que funciona em 1.2.17 e quebrada desde 1.2.18

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

@evgenyneu : Supondo que você controle o conteúdo interno e a diretiva do contêiner, descobrimos que o uso de ng-include em vez de transclude é claro, fácil e nos dá o padrão de herança que desejamos. Simplesmente passamos um nome de modelo como um atributo na declaração de diretiva externa e colocamos uma diretiva ng-include no mesmo lugar que tínhamos anteriormente a diretiva transclude. Problema resolvido.

Se você deseja que o template de conteúdo interno seja embutido no template de visão, então apenas use o ng-template para embrulhar o template na mesma posição que você estava usando antes.

@xmlilley , excelente dica, muito obrigado. Na verdade, é a abordagem mais limpa.

Aqui está a demonstração: http://plnkr.co/edit/4MwEL3?p=preview

Caras, vocês sabem que podem simplesmente fazer isso certo http://plnkr.co/edit/fw7thti1u4F9ArxsuYkQ?p=preview --- não é perfeito, mas leva você lá

@caitp , legal, obrigado. Temos muitas soluções!

Para aqueles que estão observando esse problema, criei uma diretiva ng-transclude 'aprimorada', cujo valor de atributo define o escopo interno procurado e pode ser um de 3:

  • silbing - O escopo de conteúdo transcluído é irmão do elemento onde ocorre a transclusão. Esse é o comportamento atual do ng-transcluir.
  • pai - O escopo do conteúdo transcluído é aquele do elemento onde a transclusão acontece.
  • filho - O escopo do conteúdo transcluído é o escopo filho do escopo do elemento onde a transclusão acontece.

Exemplo de uso:

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

Exemplo completo

Veja este plunk para um exemplo de todos.

A saída é assim:

image

Fonte

.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ão quebra as convenções atuais, mas adiciona uma maneira clara e declarativa de acessar voluntariamente um caso de uso comum. Obrigado!

: +1: para a solução de @Izhaki . Usarei como está, mas me sentiria muito mais confortável se fosse incluído no angular.

+1

@Izhaki +1, ótimo exemplo!

Obrigado pela diretiva transcluir personalizada. Acabei fazendo apenas uma transclusão personalizada separada, em vez de substituir o padrão. Existem algumas chamadas de função em seu patch que não tenho certeza de onde vêm. minErr () e StartingTag ()

@dehru - essas funções são internas ao AngularJS.

@Izhaki muito obrigado! Eu bifurquei seu plunk que repete o conteúdo transcluído, se alguém estiver interessado.
Vale a pena mencionar que ng-transclude = "parent" pode não funcionar da maneira que você esperava.
http://plnkr.co/edit/S6ngqz?p=preview

plunker_ng-transclude_ng-repeat

@Izhaki Muito bom.
Parece algo que deveria estar em uma solicitação de pull para angular ..
Pelo menos em uma diretiva separada com um repositório Github, você acha que pode publicá-lo (para que eu possa oferecer algumas mudanças ...)?
Obrigado

Izhaki, isso é incrível. Eu _coração_ você.

@Izhaki Esta é uma solução muito boa para o problema de escopos filho. Obrigado.

Estou confuso, porém, e gostaria de saber se você poderia explicar como a herança da função $ transclude é transportada da diretiva externa para a diretiva ngTransclude. Não é declarado explicitamente em qualquer lugar oficial que eu pude encontrar, mas presumi que transclude: true tinha que ser usado na diretiva para usar a função $ transclude na função de link. Depois de brincar um pouco com o código, descobri que usar transclude: true realmente quebra o código. O que está acontecendo aqui?

Lutei com isso por dias, bem como com o fato de que transcluir insere os elementos transcluídos dentro do espaço reservado para transcluir em vez de substituí-lo por eles.

Descobri que ng-transclude-replace trata de ambos os problemas! http://gogoout.github.io/angular-directives-utils/#/api/ng -directives-utils.transcludeReplace

Eu não preciso propagar a diretiva ng-repeat para a própria diretiva repetida ou qualquer coisa do tipo! Tudo parece estar funcionando, incluindo todas as validações / vinculações.

Aqui está um snippet do meu 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 Obrigado por mencionar ng-transclude-replace, ótima correção.

Recentemente, encontrei esse problema. Embora @Izhaki tenha trabalhado para mim, estou curioso para saber se uma prática recomendada surgiu desde esta discussão. Em particular, houve algum interesse em tornar ng-transclude="sibling | parent | child" parte do núcleo angular?

@telekid - Não temos planos de incluir esse recurso no núcleo agora.

Vejo que existem soluções e contornos, mas seria muito conveniente se houvesse uma forma de acessar o escopo interno da diretiva.

Gostaria de salientar que atualizei o mod transclude que @Izhaki criou para angular 1.5 para que funcione com transclusão multi-slot.
Filial: 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

O que acabei fazendo foi apenas usar $parent . É o armário da baunilha sem ter que acrescentar muitas coisas.

Então, eu tenho 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>

Olá @ moneytree-doug: Eu uso esta solução que você forneceu, mas acho o escopo de transcluir - este ainda é o escopo da diretiva, não o filho do novo escopo gerado por ng-repeat. Você pode me dar algumas sugestões?

@szetin Você poderia colocar um jsfiddle me mostrando o que você espera vs o que está acontecendo?

Esta página foi útil?
0 / 5 - 0 avaliações