Escopo de isolamento de diretiva com escopo de repetição de ng em AngularJS

95

Eu tenho uma diretiva com um escopo isolado (para que eu possa reutilizar a diretiva em outros lugares) e, quando uso essa diretiva com um ng-repeat, ela não funciona.

Eu li toda a documentação e as respostas do Stack Overflow sobre este tópico e compreendo os problemas. Eu acredito que evitei todas as pegadinhas usuais.

Portanto, eu entendo que meu código falha devido ao escopo criado pela ng-repeatdiretiva. Minha própria diretiva cria um escopo isolado e faz uma vinculação de dados bidirecional a um objeto no escopo pai. Minha diretiva atribuirá um novo valor de objeto a esta variável associada e isso funciona perfeitamente quando minha diretiva é usada sem ng-repeat(a variável pai é atualizada corretamente). No entanto, com ng-repeat, a atribuição cria uma nova variável no ng-repeatescopo e a variável pai não vê a mudança. Tudo isso é o esperado com base no que li.

Também li que, quando há várias diretivas em um determinado elemento, apenas um escopo é criado. E que um prioritypode ser definido em cada diretiva para definir a ordem em que as diretivas são aplicadas; as diretivas são classificadas por prioridade e, em seguida, suas funções de compilação são chamadas (procure a palavra prioridade em http://docs.angularjs.org/guide/directive ).

Então, eu esperava poder usar a prioridade para garantir que minha diretiva seja executada primeiro e acabe criando um escopo isolate e, quando for ng-repeatexecutada, reutilize o escopo isolate em vez de criar um escopo que herda prototipicamente do escopo pai. A ng-repeatdocumentação afirma que essa diretiva é executada em nível de prioridade 1000. Não está claro se 1é um nível de prioridade mais alto ou um nível de prioridade mais baixo. Quando usei nível de prioridade 1na minha diretiva, não fez diferença, então tentei 2000. Mas isso torna as coisas piores: minhas ligações bidirecionais se tornam undefinede minha diretriz não exibe nada.

Eu criei um violino para mostrar meu problema . Eu comentei a priorityconfiguração em minha diretiva. Eu tenho uma lista de objetos de nome e uma diretiva chamada name-rowque mostra os campos de nome e sobrenome no objeto de nome. Quando um nome exibido é clicado, eu quero definir uma selectedvariável no escopo principal. A matriz de nomes e a selectedvariável são passadas para a name-rowdiretiva usando vinculação de dados bidirecional.

Eu sei como fazer isso funcionar chamando funções no escopo principal. Também sei que se selectedestiver dentro de outro objeto e me vincular ao objeto externo, as coisas funcionarão. Mas não estou interessado nessas soluções no momento.

Em vez disso, as perguntas que tenho são:

  • Como evito a ng-repeatcriação de um escopo que herda prototipicamente do escopo pai e, em vez disso, faço com que ele use o escopo isolate da minha diretiva?
  • Por que o nível de prioridade 2000na minha diretiva não está funcionando?
  • Usando o Batarang, é possível saber que tipo de escopo está em uso?
Deepak Nulu
fonte
1
Normalmente, você não deseja usar um escopo isolado se sua diretiva for usada no mesmo elemento com outras diretivas. Já que você está criando suas próprias propriedades de escopo e precisa trabalhar com ng-repeat, sugiro usar scope: truepara sua diretiva. Veja também (se ainda não o fez) stackoverflow.com/questions/14914213/… Além disso, só porque uma diretiva será usada em vários lugares não significa que devemos usar automaticamente um escopo isolado.
Mark Rajcok
Eu li muitas de suas respostas (elas estão além de excelentes, obrigado por escrevê-las), mas nunca me ocorreu ler suas perguntas :-). Eu li o que você vinculou. Parece-me que as diretivas de escopo isolado não podem ser misturadas com outras diretivas. Concordo com a opinião de que tais diretivas são componentes e, portanto, não precisam de ser misturadas com outras diretivas. A única exceção (até agora) para mim seria ng-repeat. Eu acho que é valioso poder misturar diretivas autônomas com ng-repeat. Continua ...
Deepak Nulu
Continuação do anterior ... Portanto, se deveria haver apenas uma diretiva com escopo para um elemento, então ng-repeatnão deveria haver um escopo. ng-repeatter um escopo faz sentido para o caso de uso típico, portanto, não estou sugerindo que ele seja alterado. Em vez disso, como comentei na resposta de Alex Osborn, acho que vou criar uma diretiva de repetição baseada em ng-repeatque não cria seu próprio escopo. Isso pode ser usado para repetir diretivas que têm seus próprios escopos isolados. A ser continuado ...
Deepak Nulu
O código que repete uma diretiva agora precisa saber se deve usar ng-repeatou a diretiva de repetição sem escopo personalizada. Acho que está tudo bem para o "chamador" saber disso, mas não é bom para um "receptor" (a diretiva sendo repetida) saber se ela está sendo repetida ou não. A ser continuado ...
Deepak Nulu
Ficando um pouco louco com os comentários aqui ... :-) ngRepeat deve criar seu próprio escopo. Por que você acha que precisa de um escopo isolado aqui?
Josh David Miller,

Respostas:

203

Ok, através de muitos dos comentários acima, descobri a confusão. Primeiro, alguns pontos de esclarecimento:

  • ngRepeat não afeta o escopo isolado escolhido
  • os parâmetros passados para ngRepeat para uso em atributos da sua directiva não usar um escopo prototipicamente-herdada
  • a razão de sua diretiva não funcionar não tem nada a ver com o escopo isolado

Aqui está um exemplo do mesmo código, mas com a diretiva removida:

<li ng-repeat="name in names"
    ng-class="{ active: $index == selected }"
    ng-click="selected = $index">
    {{$index}}: {{name.first}} {{name.last}}
</li>

Aqui está um JSFiddle demonstrando que não funcionará. Você obtém exatamente os mesmos resultados de sua diretiva.

Por que não funciona? Porque os escopos no AngularJS usam herança prototípica. O valor selectedem seu escopo pai é um primitivo . Em JavaScript, isso significa que será sobrescrito quando um filho definir o mesmo valor. Há uma regra de ouro nos escopos AngularJS: os valores do modelo sempre devem ter um .neles. Ou seja, eles nunca devem ser primitivos. Veja esta resposta do SO para mais informações.


Aqui está uma imagem da aparência inicial dos osciloscópios.

insira a descrição da imagem aqui

Depois de clicar no primeiro item, os escopos agora ficam assim:

insira a descrição da imagem aqui

Observe que uma nova selectedpropriedade foi criada no escopo ngRepeat. O escopo do controlador 003 não foi alterado.

Você provavelmente pode adivinhar o que acontece quando clicamos no segundo item:

insira a descrição da imagem aqui


Portanto, o seu problema não é realmente causado pelo ngRepeat - é causado pela quebra de uma regra de ouro no AngularJS. A maneira de corrigir isso é simplesmente usar uma propriedade de objeto:

$scope.state = { selected: undefined };
<li ng-repeat="name in names"
    ng-class="{ active: $index == state.selected }"
    ng-click="state.selected = $index">
    {{$index}}: {{name.first}} {{name.last}}
</li>

Aqui está um segundo JSFiddle mostrando que isso também funciona.

Aqui está a aparência inicial dos escopos:

insira a descrição da imagem aqui

Depois de clicar no primeiro item:

insira a descrição da imagem aqui

Aqui, o escopo do controlador está sendo afetado, conforme desejado.

Além disso, para provar que isso ainda funcionará com sua diretiva com um escopo isolado (porque, novamente, isso não tem nada a ver com o seu problema), aqui está um JSFiddle para isso também, a visualização deve refletir o objeto. Você notará que a única mudança necessária foi usar um objeto em vez de um primitivo .

Escopos inicialmente:

insira a descrição da imagem aqui

Escopos depois de clicar no primeiro item:

insira a descrição da imagem aqui

Para concluir: mais uma vez, seu problema não é com o escopo isolado e não é como o ngRepeat funciona. O seu problema é que você está quebrando uma regra que é conhecida por causar esse mesmo problema. Os modelos no AngularJS devem sempre ter um ..

Josh David Miller
fonte
Meus experimentos com diretivas com escopos isolados e vinculação de dados bidirecional me levam a acreditar que a .regra de ouro só é necessária para escopos que têm herança prototípica. Com isolate-scopes, as variáveis ​​nas quais você está interessado são definidas na scope: {}definição da diretiva e, portanto, essas variáveis ​​existirão no isolate-scope desde o início. Além disso, eles não mascaram as variáveis ​​pai porque o escopo isolate não herda prototipicamente do escopo pai. ou seja, não há escopo "pai" para mascarar.
Deepak Nulu
1
Também deveria ter dito: sua diretiva não está criando um escopo filho, mas poderia ser facilmente usada em um contexto que requer um escopo filho. ngRepeat é um caso. A transclusão também. Confie em mim - use o ..
Josh David Miller
1
Discussão no AngularJS Google Group onde @JoshDavidMiller finalmente conseguiu esclarecer minha confusão!
Deepak Nulu
2
De onde vocês conseguem todos esses diagramas legais? Eu vi outro usuário postando isso também.
Asad Saeeduddin
3
@Asad, recentemente coloquei a ferramenta no GitHub, é chamada de Peri $ scope .
Mark Rajcok,
6

Sem tentar evitar diretamente as suas perguntas, em vez disso, dê uma olhada no seguinte violino:

http://jsfiddle.net/dVPLM/

O ponto principal é que em vez de tentar lutar e mudar o comportamento convencional do Angular, você pode estruturar sua diretiva para trabalhar ng-repeatem vez de tentar substituí-la.

Em seu modelo:

    <name-row 
        in-names-list="names"
        io-selected="selected">
    </name-row>

Em sua diretriz:

    template:
'        <ul>' +      
'            <li ng-repeat="name in inNamesList" ng-class="activeClass($index)" >' +
'                <a ng-click="setSelected($index)">' +
'                    {{$index}} - {{name.first}} {{name.last}}' +
'                </a>' +
'            </li>' +
'        </ul>'

Em resposta às suas perguntas:

  • ng-repeat irá criar um escopo, você realmente não deveria tentar mudar isso.
  • A prioridade nas diretivas não é apenas a ordem de execução - consulte: AngularJS: Como o compilador HTML organiza a ordem de compilação?
  • No Batarang, se você verificar a guia de desempenho, poderá ver as expressões vinculadas a cada escopo e verificar se isso corresponde às suas expectativas.
Alex Osborn
fonte
Obrigado por uma resposta tão rápida. Se o que estou tentando vai contra a corrente, fico um pouco triste (AngularJS é, no entanto, um framework incrível). Uma diretiva deve ser um componente reutilizável. Quando for escrito, o autor não deve se preocupar se será usado com um ng-repeatou não. Talvez quando é escrito pela primeira vez, nunca seja usado com ng-repeat. E em algum momento no futuro, ele pode se acostumar ng-repeate, nesse ponto, deve funcionar sem reescrever. Esperançosamente, uma versão futura do AngularJS tornará isso possível.
Deepak Nulu
Eu gostaria de esclarecer meu comentário acima. Eu acho que é possível escrever uma diretiva que não se preocupe se está sendo usada com ng-repeatou não. Mas parece que eu teria que passar uma função para a diretiva para que ela possa modificar as variáveis ​​no escopo pai, em vez de ser capaz de modificar uma vinculação bidirecional no próprio escopo da diretiva. Encadernação bidirecional e diretivas reutilizáveis ​​são minhas duas coisas favoritas no AngularJS e ng-repeatestá provando ser uma mosca na sopa. Talvez eu possa escrever um ng-repeatequivalente que não crie seu próprio escopo.
Deepak Nulu