web-dev-qa-db-fra.com

Utiliser une directive à l'intérieur d'un ng-repeat et un mystérieux pouvoir de portée '@'

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:

  1. Pourquoi la version longue ne fonctionne-t-elle pas dans une répétition ng?
  2. Comment pourriez-vous faire fonctionner la version longue dans une répétition ng?
60
Jonah

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 exemple src="{{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 sur undefined.

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).

101
Michelle Tilley