Angular.js: ng-transclude não deve criar um novo escopo irmão.

Criado em 20 dez. 2013  ·  69Comentários  ·  Fonte: angular/angular.js

Isso é mais uma solicitação de mudança e gostaria de ver o que outras pessoas pensam.

Na minha humilde opinião, o ng-transclude não deve criar seu próprio escopo ou pelo menos ter uma maneira de impedi-lo de fazê-lo. A razão por trás disso é que uma diretiva que solicita a transclusão já tem meios para especificar se deseja ou não ter um escopo, um escopo isolado ou nenhum escopo. Ele usa a diretiva ng-transclude para marcar onde deseja inserir o conteúdo. Quando ng-transclude cria seu próprio escopo irmão, meio que quebra as expectativas da diretiva que define que tipo de escopo ele deseja e surge a manifestação da popular confusão 'valor' versus 'objeto.valor'.

Aqui está um exemplo de onde o novo escopo não faz sentido na minha opinião:

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

Tudo o que esta diretiva deseja é substituir o <box>content</box> por um <div>content</div> e o conteúdo ter o escopo isolado.

Criar uma estrutura aninhada de diretivas como essa leva à poluição da árvore de escopo. Aqui está um exemplo de plunker (http://plnkr.co/edit/DwukVGGprFFjQuVY8yTz) de três diretivas aninhadas que criam uma estrutura de árvore de escopo 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

Esse comportamento não parece agregar valor ao seu propósito, mas cria muita confusão entre os iniciantes.

No momento, uso a seguinte solução alternativa que atinge exatamente o que o exemplo anterior faz:

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

Aqui está um exemplo de plunker (http://plnkr.co/edit/46v6IBLkhS71L1WbUDFl) que ilustra esse conceito. Isso deixa a árvore do escopo bem organizada:

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

E a associação bidirecional funciona da maneira que muitos esperam quando associam 'valor' em vez de 'objeto.valor'. (Acredito que o fato de passar apenas "valor" funcionar em alguns casos, mas não no outro, e culpar a natureza da herança prototípica em javascript não é uma boa desculpa. O fato de muitas pessoas acharem esse comportamento inesperado indica que há uma falha arquitetônica .)

Eu adoraria ouvir o que outras pessoas pensam e casos de uso em que pensam que a criação de um novo escopo irmão para ng-transclude faz sentido.

Lots of comments $compile high won't fix bug

Comentários muito úteis

Ainda bem que mudei para o Ember anos atrás. :)

Todos 69 comentários

Onde transcluir cria um novo escopo? http://plnkr.co/edit/EuHaBR26JgAegQKvwOGH?p=preview Não estou vendo

Estou falando sobre a diretiva ng-transclude. O que você tem em seu exemplo é exatamente o que meu trabalho ao redor faz.

este é um pedido válido. Estávamos considerando isso para o 1.2, mas ele estava chegando perto do lançamento final e não queríamos introduzir essa alteração significativa.

devemos considerá-lo por 1,3

Agradável! Estou feliz que você já esteja considerando isso.

1 para isso. Acho que meu problema está relacionado a isso: eu uso ng-transclude em uma diretiva com formulários e tenho que passar pelo escopo. $$ childHead para acessar o objeto de validação de formulário, mas não tenho problemas para acessar meus modelos.

Aqui está um exemplo: http://fiddle.jshell.net/39cgW/3/

1 encontrei este problema hoje e odeio jogar $parent todos os lugares.

Então, para encontrar uma solução para isso, parece que existem duas possibilidades

1) alterar a diretiva ngTransclude para especificar seu escopo (na verdade, ela poderia ser reduzida muito mais do que isso, eu acho --- nenhum controlador necessário)

ou

2) não crie um novo escopo quando o escopo não for especificado

Então, poderíamos salvar alguns bytes com a opção 1) e isso é bom, 2) seria a solução menor (exclusão de 3 linhas ou mais) e não está claro para mim se há casos de uso em que criar um novo escopo implicitamente faz sentido lá ( talvez haja, mas parece completamente contrário à forma como a transclusão é descrita nos documentos)


Ou, se você quiser ser muito sofisticado, talvez possa evitar interromper as alterações inteiramente, permitindo que o ngTransclude especifique se deseja um novo escopo ou não, por meio do valor do atributo.

Pensamentos?

Não entendo a diferença entre o 1 e o 2!

Apenas minha opinião, mas acho que a compatibilidade com versões anteriores será importante para essa mudança. Imagino que existam muitos aplicativos (incluindo o meu) que contornaram esse problema de escopo usando coisas como $ parent e scope. $$ childHead. Qualquer atualização que altere esse comportamento causará algumas dores de cabeça (mas talvez seja melhor causar dores de cabeça mais cedo ou mais tarde).

Dito isto, de um ponto de vista teórico, penso que faz mais sentido que o ng-tranclude tenha, por defeito, o mesmo âmbito que a directiva. O objetivo de transcluir o conteúdo é que você deseja que ele seja uma parte integrada do conteúdo. Há momentos em que tenho muitos aninhamentos de diretivas com transcluídos, mas ainda quero que eles se comportem como um grande componente. Tê-los com todos os escopos diferentes torna isso muito complicado.

Todos apenas meus pensamentos. No mínimo, ter a opção já será um passo acima da situação atual. :)

@troch para esclarecer, injetamos o seguinte no controlador do 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);
        }

A diretiva chama esta função sem escopo e, como tal, o escopo é indefinido ... Então, em boundTranscludeFn , ela cria um novo escopo se o transcludeScope for falso ...

Então, o que estou dizendo é que, para 1), podemos simplesmente especificar o escopo atual para a função transcluir (uma vez que esta diretiva é uma diretiva vizinha de qualquer coisa que possa ter um escopo isolado, isso ainda deve nos dar o escopo original) .

Como alternativa, 2), não crie um novo escopo e apenas padronize para o escopo atual (em createBoundTranscludeFn) (potencialmente interrompendo a mudança e potencialmente interrompendo muitos testes).

Ambos são muito simples de fazer.

+1

+1

+1

Definitivamente +1

+1

+1

+1

+1 por favor

Você sabe o que seria muito legal, embora talvez não seja totalmente factível a tempo para 1.3, os proxies ES6 poderiam tornar a transclusão muito boa --- mantendo as propriedades do escopo da transclusão, mas tendo a posição correta na hierarquia.

Se a implementação do Proxy não estiver disponível, provavelmente poderia apenas retornar ao escopo. $ New (), portanto, pode realmente ser possível fazer esse trabalho bem no início. O complicado é que a especificação é um pouco instável.

Portanto, você ainda obterá escopos indesejados se quiser que a hierarquia de escopo seja muito limpa, mas pelo menos você terá o efeito colateral de as ligações de dados não serem interrompidas inesperadamente. Eu não sei.

+1

+1

+1

+1

+1

@caitp

Ou, se você quiser ser muito sofisticado, talvez possa evitar interromper as alterações inteiramente, permitindo que o ngTransclude especifique se deseja um novo escopo ou não, por meio do valor do atributo.

Tudo o que não tiver mudanças significativas tem meu voto.

+1

+1

+1

+1

+1

+1. Sempre me perguntei sobre esse comportamento. Eu gosto desta solução do caitp:

@caitp

Ou, se você quiser ser muito sofisticado, talvez possa evitar alterar totalmente as alterações permitindo que> ngTransclude especifique se deseja um novo escopo ou não, por meio do valor do atributo.

+1

+1

+1

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 do conteúdo transcluído é irmão do elemento onde a transclusão acontece. Esse é o comportamento atual do ng-transcluir.
  • parent - O escopo do conteúdo transcluído é aquele do elemento onde a transclusão acontece.
  • child - 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;
      }
    }
  }
})

Como o problema que levantei anteriormente em # 8609 foi encerrado como uma duplicata deste tópico, estou reafirmando aqui.

Na minha opinião, a maneira atual como um escopo é criado para a parte transcluída do DOM é altamente ilógica!
Vai contra o fluxo normal em angular e deve ser corrigido!

Aqui está um trecho da minha edição anterior:

Eu criei um pequeno plunk para ilustrar meu problema aqui.

o ofensor primário está nesta diretriz

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

Quando um usuário faz algo assim:

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


O resultado será surpreendente para muitos, especialmente para os novos usuários. E este é até mesmo um caso de uso (excessivamente) simplificado. Tente exibir algumas mensagens de validação lá;)

Este é um caso de uso diferente daquele em que o problema começou, mas concordo que é basicamente o mesmo problema!

Faz muito mais sentido transcluir o escopo com o elemento dom. Se um novo escopo for realmente necessário, um usuário pode colocar um ngController no elemento ngTransclude qualquer maneira. Ou pode haver um sinalizador opcional em ngTransclude que aciona um. Esse sinalizador também pode ser usado para resolver o caso de uso # 5489. E também pode ser usado para a solução oferecida pela caitp.

+1

+1

+1

+1

+1

+1

+1

+1

+1

+1

+1

+1

A diretiva +1 parece transcluída cria escopo mesmo quando solicitada a não fazê-lo. exemplo http://plnkr.co/edit/Wn81IBkE87vtigXvjmIa?p=preview

+1

+1

+1

+1

+1

+1

+1

+1

+ 1 — existem soluções alternativas válidas para isso?

@nikkwong , há algumas soluções alternativas postadas neste tópico. Eu sei que fiz um link em um plunk que mostrou uma solução alternativa.

OK, então mesmo que isso chegue, não será até 1.5.x

Mas antes disso tenho uma preocupação. O principal motivo para a criação de um novo escopo (chame-o de irmão do escopo isolado ou, mais precisamente, filho do escopo original de onde vem o conteúdo transcluído) é evitar vazamentos de memória.

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

com uma versão que reutiliza o escopo de onde vem a transclusão original:
http://plnkr.co/edit/MXFz2awcqwQQ7R882Xwz?p=preview

Na segunda versão, conforme você ativa e desativa o conteúdo transcluído, o número de observadores continua aumentando - um vazamento de memória.

@petebacondarwin deve definitivamente ser um novo escopo para que possa ser destruído com seus observadores. Acho que a maior parte do pesar sobre a transclusão é porque ng-transclude criaria um escopo irmão do escopo contido, tornando impossível acessar suas variáveis ​​por meio da herança prototípica. Ou eu estou errado?

Nós percebemos que o problema é a maneira como a ligação angular bidirecional funciona, definitivamente ng-transclude cria um escopo filho, mas haverá inúmeras ocasiões em que você se encontrará na mesma situação, por exemplo, ng-repeat cria um escopo filho. Depois de olhar para o código angular, percebemos que o problema da ligação bidirecional não funciona bem com o atributo do objeto, mas funciona bem com os próprios objetos

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

Solução: http://plnkr.co/edit/KShClgQVwIjscXPzVRwR?p=preview

Não é perfeito, mas sabendo que isso resolveu muitos dos nossos problemas, nunca faça uma ligação bidirecional em um atributo

Observe que essa solução já foi sugerida por mbykovskyy, quando ele levantou o problema, estou apenas dando um exemplo, pois demorei um pouco para descobrir.

@petebacondarwin Será apenas um vazamento temporário, até que o escopo de retenção seja destruído. Aqui está (um golpe) [http://plnkr.co/edit/Q587WQnX0u0u7JjhtCxa?p=preview] que mostra que se você mudar o transclude-holding-scope, tudo será liberado perfeitamente.
Ainda assim, é um ponto que precisa de atenção. Talvez um grande aviso nos documentos sobre esse possível vazamento seja o suficiente para consertá-lo?

Bem, qualquer vazamento de JS é apenas temporário até que você atualize o navegador ;-)
Em 8 de setembro de 2015 16:41, "Sander Elias" [email protected] escreveu:

@petebacondarwin https://github.com/petebacondarwin Será apenas um
vazamento temporário, até que o escopo de retenção seja destruído. Aqui está um
plunk) [http://plnkr.co/edit/Q587WQnX0u0u7JjhtCxa?p=preview] que mostra
que se você mudar o transclude-holding-scope, tudo será
lançado muito bem.
Ainda assim, é um ponto que precisa de atenção. Talvez um grande Waring no
os documentos sobre esse possível vazamento podem ser suficientes para consertá-lo?

-
Responda a este e-mail diretamente ou visualize-o no GitHub
https://github.com/angular/angular.js/issues/5489#issuecomment -138603298
.

@SanderElias Os usuários de nosso aplicativo Angular estão ocupados do início ao fim do dia.
Já vemos que o uso de memória está aumentando durante o dia e devemos ter muito cuidado com o que colocamos na página, introduzir mais possíveis vazamentos é arriscado.

@troch - transclusion na verdade cria um childscope do escopo onde o conteúdo transcluído foi originalmente encontrado. Isso foi quebrado algumas versões atrás (definitivamente na 1.2) onde, em vez disso, ele apenas criou um filho do pai do escopo das diretivas atuais. Isso significava que transclusões profundamente aninhadas, na verdade, obtiveram o escopo de transclusão errado.

Tenho que concordar com @petebacondarwin , e mesmo quando o comportamento atual não é 100% intuitivo, o comportamento atual funciona melhor para evitar qualquer vazamento. Estou inclinado a fechar esse problema porque não vou consertar

Ainda bem que mudei para o Ember anos atrás. :)

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