Si vous préférez voir la question dans le code de travail, commencez ici: http://jsbin.com/ayigub/2/edit
Considérez ces façons presque équivalentes d'écrire une direcive simple:
app.directive("drinkShortcut", function() {
return {
scope: { flavor: '@'},
template: '<div>{{flavor}}</div>'
};
});
app.directive("drinkLonghand", function() {
return {
scope: {},
template: '<div>{{flavor}}</div>',
link: function(scope, element, attrs) {
scope.flavor = attrs.flavor;
}
};
});
Lorsqu'elles sont utilisées seules, les deux directives fonctionnent et se comportent de manière identique:
<!-- This works -->
<div drink-shortcut flavor="blueberry"></div>
<hr/>
<!-- This works -->
<div drink-longhand flavor="strawberry"></div>
<hr/>
Cependant, lorsqu'elle est utilisée dans une répétition ng, seule la version de raccourci fonctionne:
<!-- Using the shortcut inside a repeat also works -->
<div ng-repeat="flav in ['cherry', 'grape']">
<div drink-shortcut flavor="{{flav}}"></div>
</div>
<hr/>
<!-- HOWEVER: using the longhand inside a repeat DOESN'T WORK -->
<div ng-repeat="flav in ['cherry', 'grape']">
<div drink-longhand flavor="{{flav}}"></div>
</div>
Mes questions sont:
Dans drinkLonghand
, vous utilisez le code
scope.flavor = attrs.flavor;
Pendant la phase de liaison, les attributs interpolés n'ont pas encore été évalués, donc leurs valeurs sont undefined
. (Ils travaillent en dehors du ng-repeat
parce que dans ces cas, vous n'utilisez pas l'interpolation de chaînes; vous passez simplement une chaîne ordinaire, par exemple "fraise".) Ceci est mentionné dans le Guide du développeur de directives , ainsi qu'une méthode sur Attributes
qui n'est pas présente dans la documentation de l'API appelée $observe
:
Utilisation
$observe
pour observer les changements de valeur des attributs qui contiennent une interpolation (par exemplesrc="{{bar}}"
). Non seulement cela est très efficace, mais c'est également le seul moyen d'obtenir facilement la valeur réelle car pendant la phase de liaison, l'interpolation n'a pas encore été évaluée et la valeur est donc actuellement définie surundefined
.
Donc, pour résoudre ceci problème, votre directive drinkLonghand
devrait ressembler à ceci:
app.directive("drinkLonghand", function() {
return {
template: '<div>{{flavor}}</div>',
link: function(scope, element, attrs) {
attrs.$observe('flavor', function(flavor) {
scope.flavor = flavor;
});
}
};
});
Cependant, le problème avec ceci est qu'il n'utilise pas une portée isolée; ainsi, la ligne
scope.flavor = flavor;
a le potentiel d'écraser une variable préexistante sur la portée nommée flavor
. L'ajout d'une étendue d'isolat vide ne fonctionne pas non plus; c'est parce que Angular tente d'interpoler la chaîne en fonction de la portée de la directive, sur laquelle il n'y a pas d'attribut appelé flav
. (Vous pouvez tester cela en ajoutant scope.flav = 'test';
au-dessus de l'appel à attrs.$observe
.)
Bien sûr, vous pouvez résoudre ce problème avec une définition de portée isolée comme
scope: { flav: '@flavor' }
ou en créant une portée enfant non isolée
scope: true
ou en ne s'appuyant pas sur un template
avec {{flavor}}
et faire à la place une manipulation directe du DOM comme
attrs.$observe('flavor', function(flavor) {
element.text(flavor);
});
mais cela va à l'encontre du but de l'exercice (par exemple, il serait plus facile d'utiliser simplement la méthode drinkShortcut
). Donc, pour faire fonctionner cette directive, nous allons décomposer $interpolate
service pour effectuer nous-mêmes l'interpolation sur la directive $parent
portée:
app.directive("drinkLonghand", function($interpolate) {
return {
scope: {},
template: '<div>{{flavor}}</div>',
link: function(scope, element, attrs) {
// element.attr('flavor') == '{{flav}}'
// `flav` is defined on `scope.$parent` from the ng-repeat
var fn = $interpolate(element.attr('flavor'));
scope.flavor = fn(scope.$parent);
}
};
});
Bien sûr, cela ne fonctionne que pour la valeur initiale de scope.$parent.flav
; si la valeur peut changer, vous devrez tilisez $watch
et réévaluer le résultat de la fonction d'interpolation fn
(je ne sais pas du haut de ma tête comment tu saurais quoi _ $watch
; vous devrez peut-être simplement passer une fonction). scope: { flavor: '@' }
est un raccourci sympa pour éviter d'avoir à gérer toute cette complexité.
[Mise à jour]
Pour répondre à la question des commentaires:
Comment la méthode de raccourci résout-elle ce problème dans les coulisses? Utilise-t-il le service $ interpolate comme vous l'avez fait, ou fait-il autre chose?
Je n'en étais pas sûr, alors j'ai regardé dans la source. J'ai trouvé ce qui suit dans compile.js
:
forEach(newIsolateScopeDirective.scope, function(definiton, scopeName) {
var match = definiton.match(LOCAL_REGEXP) || [],
attrName = match[2]|| scopeName,
mode = match[1], // @, =, or &
lastValue,
parentGet, parentSet;
switch (mode) {
case '@': {
attrs.$observe(attrName, function(value) {
scope[scopeName] = value;
});
attrs.$$observers[attrName].$$scope = parentScope;
break;
}
Il semble donc que attrs.$observe
peut être dit en interne d'utiliser une portée différente de celle actuelle pour baser l'observation d'attribut (l'avant-dernière ligne, au-dessus de break
). Bien qu'il puisse être tentant de l'utiliser vous-même, gardez à l'esprit que tout ce qui concerne le double dollar $$
le préfixe doit être considéré comme privé pour l'API privée d'Angular et peut être modifié sans avertissement (sans oublier que vous l'obtenez gratuitement de toute façon lorsque vous utilisez le @
mode).