Directive isoler la portée avec la portée ng-repeat dans AngularJS

95

J'ai une directive avec une portée isolée (pour que je puisse réutiliser la directive dans d'autres endroits), et lorsque j'utilise cette directive avec un ng-repeat, cela ne fonctionne pas.

J'ai lu toute la documentation et les réponses de Stack Overflow sur ce sujet et je comprends les problèmes. Je crois que j'ai évité tous les pièges habituels.

Je comprends donc que mon code échoue à cause de la portée créée par la ng-repeatdirective. Ma propre directive crée une portée isolate et effectue une liaison de données bidirectionnelle à un objet de la portée parent. Ma directive attribuera une nouvelle valeur d'objet à cette variable liée et cela fonctionne parfaitement lorsque ma directive est utilisée sans ng-repeat(la variable parente est mise à jour correctement). Cependant, avec ng-repeat, l'affectation crée une nouvelle variable dans la ng-repeatportée et la variable parent ne voit pas le changement. Tout cela est comme prévu d'après ce que j'ai lu.

J'ai également lu que lorsqu'il y a plusieurs directives sur un élément donné, une seule portée est créée. Et que a prioritypeut être défini dans chaque directive pour définir l'ordre dans lequel les directives sont appliquées; les directives sont triées par priorité et ensuite leurs fonctions de compilation sont appelées (recherchez le mot priority sur http://docs.angularjs.org/guide/directive ).

J'espérais donc pouvoir utiliser la priorité pour m'assurer que ma directive s'exécute en premier et finit par créer une portée isolate, et lorsqu'elle ng-repeats'exécute, elle réutilise la portée isolate au lieu de créer une portée qui hérite prototypiquement de la portée parent. La ng-repeatdocumentation indique que cette directive s'exécute au niveau de priorité 1000. Il n'est pas clair s'il 1s'agit d'un niveau de priorité plus élevé ou d'un niveau de priorité inférieur. Lorsque j'ai utilisé le niveau de priorité 1dans ma directive, cela ne faisait aucune différence, alors j'ai essayé 2000. Mais cela ne fait qu'empirer les choses: mes liaisons bidirectionnelles deviennent undefinedet ma directive n'affiche rien.

J'ai créé un violon pour montrer mon problème . J'ai commenté le prioritycadre de ma directive. J'ai une liste d'objets de nom et une directive appelée name-rowqui montre les champs de nom et prénom dans l'objet de nom. Lorsqu'un nom affiché est cliqué, je veux qu'il définisse une selectedvariable dans la portée principale. Le tableau de noms, la selectedvariable sont passés à la name-rowdirective en utilisant une liaison de données bidirectionnelle.

Je sais comment faire fonctionner cela en appelant des fonctions dans la portée principale. Je sais aussi que si se selectedtrouve à l'intérieur d'un autre objet et que je me lie à l'objet extérieur, les choses fonctionneraient. Mais je ne suis pas intéressé par ces solutions pour le moment.

Au lieu de cela, les questions que j'ai sont:

  • Comment puis-je empêcher ng-repeatde créer une étendue qui hérite de manière prototypique de l'étendue parente et lui faire utiliser à la place l'étendue isolate de ma directive?
  • Pourquoi le niveau de priorité 2000dans ma directive ne fonctionne-t-il pas?
  • En utilisant Batarang, est-il possible de savoir quel type de lunette est utilisé?
Deepak Nulu
la source
1
Normalement, vous ne souhaitez pas utiliser une portée isolate si votre directive sera utilisée sur le même élément avec d'autres directives. Puisque vous créez vos propres propriétés de portée et que vous devez travailler avec ng-repeat, je suggère d'utiliser scope: truepour votre directive. Voir aussi (si vous ne l'avez pas déjà fait) stackoverflow.com/questions/14914213/... De plus, ce n'est pas parce qu'une directive sera utilisée à plusieurs endroits que nous devrions automatiquement utiliser une portée isolate.
Mark Rajcok
J'ai lu beaucoup de vos réponses (elles sont au-delà d'excellentes, merci de les avoir écrites), mais il ne m'est jamais venu à l'esprit de lire vos questions :-). J'ai lu ce à quoi vous avez lié. Il me semble que les directives isolées ne peuvent pas être mélangées avec d’autres directives. Je suis d’accord avec le sentiment que ces directives sont des composants et qu’elles n’ont donc pas besoin d’être mélangées avec d’autres directives. La seule exception (jusqu'à présent) pour moi serait ng-repeat. Je pense qu'il est utile de pouvoir mélanger des directives autonomes avec ng-repeat. A suivre ...
Deepak Nulu
Suite d'en haut ... Donc, s'il ne devait y avoir qu'une seule directive avec une portée pour un élément, alors ng-repeatne devrait pas avoir de portée. ng-repeatavoir une portée a du sens pour le cas d'utilisation typique, donc je ne suggère pas qu'il soit changé. Au lieu de cela, comme je l'ai commenté dans la réponse d'Alex Osborn, je pense que je vais créer une directive répétée sur ng-repeatcette base qui ne crée pas sa propre portée. Cela peut ensuite être utilisé pour répéter des directives qui ont leurs propres étendues isolées. À suivre ...
Deepak Nulu
Le code qui répète une directive a maintenant besoin de savoir s'il faut utiliser ng-repeatou la directive de répétition sans portée personnalisée. Je pense qu'il est normal que "l'appelant" sache cela, mais ce n'est pas correct pour un "appelé" (la directive étant répétée) de savoir si elle est répétée ou non. À suivre ...
Deepak Nulu
Être un peu fou avec les commentaires ici ... :-) ngRepeat doit créer sa propre portée. Pourquoi pensez-vous avoir besoin d'une lunette isolée ici?
Josh David Miller

Réponses:

203

D'accord, à travers beaucoup de commentaires ci-dessus, j'ai découvert la confusion. Tout d'abord, quelques points de clarification:

  • ngRepeat n'affecte pas la portée d'isolat choisie
  • les paramètres passés dans ngRepeat pour une utilisation sur les attributs de votre directive n'utilisent une portée prototypiquement héritée
  • la raison pour laquelle votre directive ne fonctionne pas n'a rien à voir avec la portée d'isolat

Voici un exemple du même code mais avec la directive supprimée:

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

Voici un JSFiddle démontrant que cela ne fonctionnera pas. Vous obtenez exactement les mêmes résultats que dans votre directive.

Pourquoi ça ne marche pas? Parce que les étendues dans AngularJS utilisent l'héritage prototypique. La valeur selectedde votre étendue parent est une primitive . En JavaScript, cela signifie qu'il sera écrasé lorsqu'un enfant définit la même valeur. Il existe une règle d'or dans les étendues AngularJS: les valeurs de modèle doivent toujours avoir un .. Autrement dit, ils ne devraient jamais être des primitifs. Voir cette réponse SO pour plus d'informations.


Voici une image de ce à quoi ressemblent initialement les oscilloscopes.

entrez la description de l'image ici

Après avoir cliqué sur le premier élément, les étendues ressemblent maintenant à ceci:

entrez la description de l'image ici

Notez qu'une nouvelle selectedpropriété a été créée sur l'étendue ngRepeat. La portée du contrôleur 003 n'a pas été modifiée.

Vous pouvez probablement deviner ce qui se passe lorsque nous cliquons sur le deuxième élément:

entrez la description de l'image ici


Donc, votre problème n'est pas du tout causé par ngRepeat - il est causé par une violation d'une règle d'or dans AngularJS. La façon de résoudre ce problème consiste simplement à utiliser une propriété d'objet:

$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>

Voici un deuxième JSFiddle montrant que cela fonctionne aussi.

Voici à quoi ressemblent les portées initialement:

entrez la description de l'image ici

Après avoir cliqué sur le premier élément:

entrez la description de l'image ici

Ici, la portée du contrôleur est affectée, comme souhaité.

Aussi, pour prouver que cela fonctionnera toujours avec votre directive avec une portée d'isolat (car, encore une fois, cela n'a rien à voir avec votre problème), voici un JSFiddle pour cela aussi, la vue doit refléter l'objet. Vous noterez que le seul changement nécessaire était d'utiliser un objet au lieu d'une primitive .

Portées initialement:

entrez la description de l'image ici

Scopes après avoir cliqué sur le premier élément:

entrez la description de l'image ici

Pour conclure: encore une fois, votre problème n'est pas avec la portée d'isolat et ce n'est pas avec le fonctionnement de ngRepeat. Votre problème est que vous enfreignez une règle connue pour mener à ce problème. Les modèles dans AngularJS doivent toujours avoir un fichier ..

Josh David Miller
la source
Mes expériences avec des directives avec des étendues isolées et une liaison de données bidirectionnelle m'amènent à croire que la .règle d'or n'est requise que pour les étendues qui ont un héritage prototypique. Avec isolate-scopes, les variables qui vous intéressent sont définies dans la scope: {}définition de la directive, et donc ces variables existeront dans l'isolate-scope depuis le début. En outre, ils ne masquent pas les variables parentes, car la portée isolate n'hérite pas de manière prototypique de la portée parent. c'est-à-dire qu'il n'y a pas de portée "parent" à masquer.
Deepak Nulu
1
J'aurais dû aussi dire: votre directive ne crée pas de portée enfant, mais elle pourrait facilement être utilisée dans un contexte qui nécessite une portée enfant. ngRepeat est un cas. La transclusion aussi. Croyez-moi - utilisez le ..
Josh David Miller
1
Discussion sur le groupe Google AngularJS où @JoshDavidMiller a enfin pu dissiper ma confusion!
Deepak Nulu
2
D'où tirez-vous tous ces schémas sympas? J'ai vu un autre utilisateur les publier également.
Asad Saeeduddin
3
@Asad, je viens de mettre en place l'outil sur GitHub, il s'appelle Peri $ scope .
Mark Rajcok
6

Sans essayer directement d'éviter de répondre à vos questions, jetez plutôt un œil au violon suivant:

http://jsfiddle.net/dVPLM/

Le point clé est qu'au lieu d'essayer de lutter et de changer le comportement conventionnel d'Angular, vous pouvez structurer votre directive pour travailler avec ng-repeatplutôt que d'essayer de la remplacer.

Dans votre modèle:

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

Dans votre directive:

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

En réponse à vos questions:

  • ng-repeat créera une portée, vous ne devriez vraiment pas essayer de changer cela.
  • La priorité dans les directives n'est pas seulement l'ordre d'exécution - voir: AngularJS: Comment le compilateur HTML organise-t-il l'ordre de compilation?
  • Dans Batarang, si vous vérifiez l'onglet performances, vous pouvez voir les expressions liées pour chaque portée et vérifier si cela correspond à vos attentes.
Alex Osborn
la source
Merci pour une réponse aussi rapide. Si ce que je tente va à contre-courant, je suis un peu attristé (AngularJS est néanmoins un framework génial). Une directive est censée être un composant réutilisable. Lorsqu'il est écrit, l'auteur ne devrait pas avoir à se soucier de savoir s'il sera utilisé avec un ng-repeatou non. Peut-être que lorsqu'il est écrit pour la première fois, il n'est jamais utilisé avec ng-repeat. Et dans le futur, il pourrait être utilisé avec ng-repeat, et à ce moment-là, il devrait simplement fonctionner sans réécriture. Espérons qu'une future version d'AngularJS rendra cela possible.
Deepak Nulu
Je voudrais clarifier mon commentaire ci-dessus. Je pense qu'il est possible d'écrire une directive qui ne se soucie pas de savoir si elle est utilisée avec ng-repeatou non. Mais il semble que je devrais passer une fonction à la directive afin qu'elle puisse modifier les variables dans la portée parent, au lieu de pouvoir muter une liaison bidirectionnelle dans la propre portée de la directive. La liaison bidirectionnelle et les directives réutilisables sont mes deux choses préférées à propos d'AngularJS et ng-repeatse révèlent être une mouche dans la pommade. Peut-être que je peux écrire un ng-repeatéquivalent qui ne crée pas sa propre portée.
Deepak Nulu