Ao escrever uma diretiva no AngularJS, como decido se não preciso de um novo escopo, um novo escopo filho ou um novo escopo isolado?

265

Estou procurando algumas diretrizes que podem ser usadas para ajudar a determinar qual tipo de escopo usar ao escrever uma nova diretiva. Idealmente, eu gostaria de algo semelhante a um fluxograma que me guie por várias perguntas e apareça a resposta correta - nenhum novo novo escopo, novo escopo filho ou novo escopo isolado - mas isso provavelmente está pedindo muito. Aqui está meu atual conjunto insignificante de diretrizes:

Estou ciente de que o uso de uma diretiva com um escopo isolado em um elemento força todas as outras diretivas desse mesmo elemento a usar o mesmo (um) escopo isolado, portanto, isso não limita severamente quando um escopo isolado pode ser usado?

Espero que alguns membros da equipe da Angular-UI (ou outros que escreveram muitas diretivas) possam compartilhar suas experiências.

Por favor, não adicione uma resposta que simplesmente diga "use um escopo isolado para componentes reutilizáveis".

Mark Rajcok
fonte
por "escopo filho", você quer dizer criar escopo na função de link por "scope. $ new ()"? Porque eu como saber, a Directiva pode ter isolado escopo ou não tê-lo (assim estará usando escopo onde está sendo utilizado)
Valentyn Shybanov
3
A configuração @ValentynShybanov scope: truecriará um escopo filho usando $scope.new()automaticamente.
Josh David Miller
2
@ Valentyn, o que Josh disse: então, as três possibilidades são scope: false(o padrão, sem novo escopo), scope: true(novo escopo que herda prototipicamente) e scope: { ... }(novo escopo isolado).
Mark Rajcok
1
Sim, thnx. Perdi a diferença entre "true" e "{}". Bom saber.
Valentyn Shybanov
Há um quarto caso em que as pessoas geralmente tendem a ignorar .. que é o "controlador de directiva" .. Acho que a questão deve ser expandido para incluí-los como bem ... +1 à pergunta ..
ganaraj

Respostas:

291

Que ótima pergunta! Eu amo ouvir o que os outros têm a dizer, mas aqui estão as diretrizes que eu uso.

A premissa de alta altitude: escopo é usada como a "cola" que usamos para nos comunicar entre o controlador pai, a diretiva e o modelo de diretiva.

Escopo pai:, scope: false portanto, nenhum novo escopo

Eu não uso isso com muita frequência, mas como o @MarkRajcok disse, se a diretiva não acessa nenhuma variável de escopo (e obviamente não define nenhuma!), Então isso é bom para mim. Isso também é útil para diretivas filho que são usadas apenas no contexto da diretiva pai (embora sempre haja exceções a isso) e que não tenham um modelo. Basicamente, qualquer coisa com um modelo não pertence ao compartilhamento de um escopo, porque você está expondo inerentemente esse escopo para acesso e manipulação (mas tenho certeza de que há exceções a essa regra).

Como exemplo, criei recentemente uma diretiva que desenha um gráfico vetorial (estático) usando uma biblioteca SVG que estou escrevendo. Ele possui $observedois atributos ( widthe height) e os utiliza em seus cálculos, mas não define nem lê nenhuma variável de escopo e não possui modelo. Este é um bom caso de uso para não criar outro escopo; nós não precisamos de um, então por que se preocupar?

Porém, em outra diretiva SVG, eu exigi um conjunto de dados para usar e, adicionalmente, tive que armazenar um pouquinho de estado. Nesse caso, o uso do escopo pai seria irresponsável (novamente, de um modo geral). Então, ao invés ...

Escopo da criança: scope: true

As diretivas com um escopo filho são sensíveis ao contexto e se destinam a interagir com o escopo atual.

Obviamente, uma vantagem importante disso em relação a um escopo isolado é que o usuário é livre para usar a interpolação em qualquer atributo que desejar; por exemplo, usar class="item-type-{{item.type}}"uma diretiva com um escopo isolado não funcionará por padrão, mas funciona bem em uma com um escopo filho, porque o que for interpolado ainda pode, por padrão, ser encontrado no escopo pai. Além disso, a própria diretiva pode avaliar com segurança atributos e expressões no contexto de seu próprio escopo sem se preocupar com poluição ou danos aos pais.

Por exemplo, uma dica de ferramenta é algo que apenas é adicionado; um escopo isolado não funcionaria (por padrão, veja abaixo) porque é esperado que usemos outras diretivas ou atributos interpolados aqui. A dica de ferramenta é apenas um aprimoramento. Mas a dica de ferramenta também precisa definir algumas coisas no escopo para usar com uma sub-diretiva e / ou modelo e, obviamente, gerenciar seu próprio estado, portanto seria muito ruim usar o escopo pai. Nós estamos poluindo ou danificando, e nem é bueno.

Eu me pego usando escopos filhos com mais frequência do que isolados ou escopos parentais.

Isolar o escopo: scope: {}

Isto é para componentes reutilizáveis. :-)

Mas, falando sério, penso em "componentes reutilizáveis" como "componentes independentes". A intenção é que eles sejam usados ​​para uma finalidade específica, portanto, combiná-los com outras diretivas ou adicionar outros atributos interpolados ao nó DOM inerentemente não faz sentido.

Para ser mais específico, tudo o que é necessário para essa funcionalidade autônoma é fornecido por meio de atributos especificados avaliados no contexto do escopo pai; elas são cadeias de mão única ('@'), expressões de mão única ('&') ou ligações de variável de mão dupla ('=').

Em componentes independentes, não faz sentido precisar aplicar outras diretivas ou atributos nele, porque ele existe por si só. Seu estilo é regido por seu próprio modelo (se necessário) e pode ter o conteúdo apropriado transcluído (se necessário). É autônomo, portanto, colocamos em um escopo isolado também para dizer: "Não mexa com isso. Estou fornecendo uma API definida por esses poucos atributos".

Uma boa prática é excluir o máximo possível de itens baseados em modelos das funções de link e controlador de diretiva. Isso fornece outro ponto de configuração "semelhante à API": o usuário da diretiva pode simplesmente substituir o modelo! A funcionalidade permaneceu a mesma e sua API interna nunca foi tocada, mas podemos mexer com o estilo e a implementação do DOM o quanto for necessário. ui / bootstrap é um ótimo exemplo de como fazer isso bem, porque Peter e Pawel são incríveis.

Os escopos de isolamento também são ótimos para uso com transclusão. Tome guias; eles não são apenas toda a funcionalidade, mas tudo o que está dentro dela pode ser avaliado livremente no escopo pai, deixando as guias (e os painéis) para fazer o que quiserem. As guias claramente têm seu próprio estado , que pertence ao escopo (para interagir com o modelo), mas esse estado não tem nada a ver com o contexto em que foi usado - é inteiramente interno ao que faz de uma diretiva de guia uma diretiva de guia. Além disso, não faz muito sentido usar outras diretivas com as guias. São guias - e já temos essa funcionalidade!

Envolva-o com mais funcionalidade ou transclua mais funcionalidade, mas a diretiva é o que já é.

Dito isso, devo observar que existem maneiras de contornar algumas das limitações (ou seja, recursos) de um escopo isolado, como o @ProLoser sugeriu em sua resposta. Por exemplo, na seção escopo filho, mencionei a interpolação em atributos não-diretivos que quebram ao usar um escopo isolado (por padrão). Mas o usuário poderia, por exemplo, simplesmente usar class="item-type-{{$parent.item.type}}"e funcionaria mais uma vez. Portanto, se houver um motivo convincente para usar um escopo isolado em relação a um escopo filho, mas você estiver preocupado com algumas dessas limitações, saiba que pode contornar praticamente todas elas, se necessário.

Resumo

Diretivas sem novo escopo são somente leitura; eles são totalmente confiáveis ​​(ou seja, internos ao aplicativo) e não tocam no conector. Diretivas com escopo filho adicionam funcionalidade, mas elas não são a única funcionalidade. Por fim, os escopos isolados são para diretivas que são o objetivo inteiro; eles são independentes, então não há problema (e é mais "correto") deixá-los desonestos.

Eu queria expressar meus pensamentos iniciais, mas, ao pensar em mais coisas, atualizo isso. Mas caramba - isso é tempo para uma resposta SO ...


PS: Totalmente tangencial, mas como estamos falando de escopos, prefiro dizer "prototípico", enquanto outros preferem "prototípico", o que parece ser mais preciso, mas não sai nada bem da língua. :-)

Josh David Miller
fonte
Obrigado Josh, ótima resposta. Eu queria / esperava respostas longas para isso. Duas coisas que não segui: 1) escopo filho: "o usuário é livre para usar a interpolação em qualquer atributo que desejar". 2) isolar escopo: "ou não todos, no caso de '?'" Você pode elaborar um pouco sobre eles? (Sinta-se livre para editar o seu post em vez de escrever comentários se for mais fácil.)
Mark Rajcok
@ MarkRajcok Para (1), eu mudei para torná-lo um pouco menos nebuloso - deixe-me saber se não tive êxito. Para (2), isso foi uma combinação de erro de digitação e redação ruim; Reescrevi esse parágrafo para ficar mais claro. Também adicionei um ou dois exemplos adicionais, esclareci mais algumas coisas e corrigi alguns erros de digitação.
Josh David Miller
Como mencionado na resposta - o bootstrap para angular é um ótimo exemplo de combinação destes. I encontrado o exemplo acordeão particularmente útil - GitHub - acordeão
calma
Você mencionou que usa escopos menores, achei que o padrão reutilizável de diretivas era o mais comum e evitei escrever diretivas que deveriam ser usadas apenas uma vez. Isso é desnecessário? Às vezes, quando meu HTML fica muito grande, sinto vontade de mudar essa seção para uma diretiva, mas ela será usada apenas uma vez, então deixo no html.
user2483724
2
@ user2483724 Um equívoco muito comum é que diretivas "reutilizáveis" são aquelas que usam um escopo isolado; não tão. Se você olhar para as diretrizes pré-empacotadas, quase nenhuma delas usa escopos isolados - alguns nem mesmo um escopo filho -, mas garanto que eles são reutilizáveis! A regra deve estar em como o escopo dentro da diretiva é usado. Se se trata de economizar espaço em um arquivo, não tenho certeza se uma diretiva é a melhor abordagem. Aumenta o tempo de processamento para o desenvolvedor. Mas se você precisar, então vá em frente. Ou use um ngInclude. Ou faça isso como parte de sua compilação. Muitas opções!
Josh David Miller
52

Minha política e experiência pessoal:

Isolado: uma caixa de areia privada

Quero criar muitos métodos e variáveis ​​de escopo que são SOMENTE usados ​​pela minha diretiva e nunca são vistos ou acessados ​​diretamente pelo usuário. Quero colocar na lista branca quais dados do escopo estão disponíveis para mim. Eu posso usar a transclusão para permitir que o usuário volte ao escopo pai (não afetado) . Eu não quero que meus variáveis e métodos acessíveis em crianças transcluídas.

Filho: uma subseção de conteúdo

Eu quero criar métodos e variáveis ​​de escopo que PODEM ser acessados ​​pelo usuário, mas não são relevantes para os escopos circundantes (irmãos e pais) fora do contexto da minha diretiva. Eu também gostaria de permitir que TODOS os dados do escopo pai fiquem transparentes.

Nenhuma: diretivas simples, somente leitura

Eu realmente não preciso mexer com métodos ou variáveis ​​de escopo. Provavelmente estou fazendo algo que não tem a ver com escopos (como exibir plugins jQuery simples, validação etc.).

Notas

  • Você não deve deixar que o ngModel ou outras coisas afetem diretamente sua decisão. Você pode contornar comportamentos estranhos fazendo coisas como ng-model=$parent.myVal(criança) ou ngModel: '='(isolado).
  • Isolar + transcluir restaurará todo o comportamento normal das diretivas dos irmãos e retornará ao escopo pai. Portanto, não deixe que isso afete seu julgamento.
  • Não mexa com o escopo em nenhum, porque é como colocar dados no escopo para a metade inferior do DOM, mas não a metade superior, o que faz 0 sentido.
  • Preste atenção às prioridades da diretiva (não tenha exemplos concretos de como isso pode afetar as coisas)
  • Injete serviços ou use controladores para se comunicar entre diretivas com qualquer tipo de escopo. Você também pode fazer require: '^ngModel'para procurar nos elementos-pai.
ProLoser
fonte
1
Talvez eu tenha entendido mal essa parte: "Isolar + transcluir restaurará todo comportamento normal às diretrizes dos irmãos". Veja este afunilador . Você terá que procurar no console.
Josh David Miller
1
Obrigado ProLoser por suas idéias / resposta. Você é uma das pessoas que eu esperava ver este post se eu adicionasse a tag angularjs-ui.
Mark Rajcok
@JoshDavidMiller ao falar sobre diretivas no mesmo elemento DOM, as coisas ficam mais complicadas e você deve começar a dar uma olhada na propriedade priority. A transclusão é mais relevante para o conteúdo infantil.
ProLoser
1
@ ProLoser Certo, mas não sei ao certo o que você quis dizer com essa afirmação. Obviamente, elas afetam as crianças, mas como os escopos das diretivas afetam as diretrizes dos irmãos?
Josh David Miller
18

Depois de escrever muitas diretivas, decidi usar menos isolated escopo. Embora seja legal e você encapsule os dados e não vaze dados para o escopo pai, isso limita severamente a quantidade de diretivas que você pode usar em conjunto. Assim,

Se a diretiva que você vai escrever vai se comportar inteiramente por conta própria e você não está indo para compartilhá-lo com outras directivas, ir para escopo isolado . (como um componente, você pode simplesmente conectá-lo, sem muita personalização para o desenvolvedor final) (fica muito mais complicado quando você tenta escrever subelementos que possuem diretivas)

Se a diretiva que você escreverá fará apenas manipulações dom que não precisam de um estado interno de escopo ou alterações explícitas de escopo (principalmente coisas muito simples); vá para nenhum novo escopo . (tais como ngShow, ngMouseHover, ngClick, ngRepeat)

Se a diretiva que você vai escrever precisar alterar alguns elementos no escopo pai, mas também precisar lidar com algum estado interno, vá para o novo escopo filho . (como ngController)

Não deixe de conferir o código-fonte para obter diretrizes: https://github.com/angular/angular.js/tree/master/src/ng/directive
Isso ajuda muito em como pensar sobre elas

Umur Kontacı
fonte
Se vários componentes precisarem se comunicar, eles podem ter escopo e uso isolados require, mantendo assim suas diretrizes ainda dissociadas. Então, como isso limita as possibilidades? Torna ainda mais diretivas mais específicas (então declare do que você depende). Portanto, deixaria apenas uma regra: se sua diretiva tiver estado ou precisar de alguns dados do escopo em que é usado - use o escopo isolado. Caso contrário, não use o escopo. E sobre "escopos infantis" - também escrevi muitas diretivas e nunca precisei desse recurso. Se "precisar alterar alguns elementos no escopo pai" - use as ligações.
Valentyn Shybanov
E também sobre "precisa alterar alguns elementos no escopo pai" - se você modificar algo no escopo filho, as alterações não serão preenchidas no escopo pai (a menos que você use o $parenthack sujo ). Então, na verdade, "escopos filho" para diretivas é algo que parece que deve ser usado bastante na retaguarda - como ngRepeatisso cria novos escopos filhos para cada item repetir (mas também cria usando scope.$new();e não) scope: true.
Valentyn Shybanov
1
Você não pode solicitar vários escopos isolados no mesmo elemento, não pode acessar as funções no escopo pai, a menos que as vincule explicitamente. (Boa sorte usando ngClicketc.) A exigência cria um tipo de dissociação, eu concordo, mas você ainda precisa conhecer a diretiva pai. A menos que seja como um componente , sou contra o isolamento. As diretivas (pelo menos, a maioria delas) devem ser altamente reutilizáveis ​​e o isolamento quebra isso.
Kontacı
Eu também não uso escopos filhos nas diretivas, mas como um escopo filho herda prototipicamente do escopo pai, se o acesso a uma propriedade dentro de uma propriedade no escopo pai, as alterações são preenchidas. Os autores de Angular falaram sobre isso no encontro da MTV, é "bom ter um ponto em algum lugar" youtube.com/watch?v=ZhfUv0spHCY #
376 Umur Kontacı
5
Primeiro, acho que você é um pouco duro demais em escopos isolados. Eu acho que eles têm uma aplicabilidade mais ampla do que você lhes dá crédito e que existem maneiras de evitar muitos dos desafios que você (corretamente) apontou que enfrentamos ao usá-los. Também não concordo com "pouca personalização para o desenvolvedor final" - veja minha resposta para obter detalhes. Dito isto, sua resposta não foi ruim nem errada e abordou a questão, por isso não tenho certeza do motivo pelo qual foi negado o voto. Então, +1.
Josh David Miller
9

Apenas pensei em adicionar meu entendimento atual e como ele se relaciona com outros conceitos de JS.

Padrão (por exemplo, não declarado ou escopo: false)

Isso é filosoficamente equivalente ao uso de variáveis ​​globais. Sua diretiva pode acessar tudo no controlador pai, mas também está afetando-os e sendo afetados ao mesmo tempo.

escopo:{}

É como um módulo, qualquer coisa que ele queira usar precisa ser passada explicitamente. Se TODAS as diretivas usadas forem um escopo isolado, pode ser o equivalente a criar TODAS as limas JS que você escreve seu próprio módulo com muita sobrecarga ao injetar todas as dependências.

escopo: filho

Este é o meio termo entre variáveis ​​globais e passagem explícita. É semelhante à cadeia de protótipos do javascript e apenas estende uma cópia do escopo pai. Se você criar um escopo isolado e transmitir todos os atributos e funções do escopo pai, ele será funcionalmente equivalente a isso.


A chave é que QUALQUER diretiva pode ser escrita de qualquer maneira. As diferentes declarações de escopo estão aqui apenas para ajudá-lo a organizar. Você pode transformar tudo em um módulo ou apenas usar todas as variáveis ​​globais e ter muito cuidado. Para facilitar a manutenção, é preferível modularizar sua lógica em partes logicamente coerentes. Existe um equilíbrio entre um campo aberto e uma prisão fechada. A razão pela qual isso é complicado, acredito, é que, quando as pessoas aprendem sobre isso, pensam que estão aprendendo sobre como as diretivas funcionam, mas na verdade estão aprendendo sobre organização de código / lógica.

Outra coisa que me ajudou a descobrir como as diretivas funcionam é aprender sobre o ngInclude. O ngInclude ajuda a incluir parciais html. Quando comecei a usar diretivas, descobri que você poderia usar a opção de modelo para reduzir seu código, mas eu realmente não estava anexando nenhuma lógica.

É claro que entre as diretrizes da angular e o trabalho da equipe da angular-ui , ainda não tive que criar minha própria diretiva que faça algo substancial, de modo que minha visão sobre isso pode estar completamente errada.

user2483724
fonte
2

Eu concordo com Umur. Em teoria, os escopos isolados soam maravilhosos e "portáteis", mas ao criar meu aplicativo para envolver funcionalidades não triviais, deparei-me com a necessidade de incorporar várias diretivas (algumas aninhadas dentro de outras ou adicionando atributos a elas) para poder escrever totalmente em meu próprio HTML, que é o objetivo de uma linguagem específica de domínio.

No final, é muito estranho ter que passar todos os valores globais ou compartilhados da cadeia com vários atributos em cada chamada de DOM de uma diretiva (conforme é necessário no escopo isolado). Parece estúpido escrever repetidamente tudo isso no DOM e parece ineficiente, mesmo que sejam objetos compartilhados. Também complica desnecessariamente as declarações da diretiva. A solução alternativa do uso de $ parent para "alcançar" e capturar valores da diretiva HTML parece muito ruim.

Eu também acabei alterando meu aplicativo para ter principalmente diretivas de escopo filho com muito poucos isolados - apenas aqueles que não precisam acessar QUALQUER COISA do pai, além do que podem ser passados ​​por atributos simples e não repetitivos.

Tendo sonhado o sonho de idiomas específicos de domínio por décadas antes de existir algo assim, fico feliz que o AngularJS ofereça essa opção e sei que, à medida que mais desenvolvedores trabalharem nessa área, veremos aplicativos muito interessantes que também é fácil para seus arquitetos escrever, expandir e depurar.

- D

Ungallery
fonte