Используя директиву внутри ng-repeat и таинственную силу видимости "@",

Если вы предпочитаете видеть вопрос в рабочем коде, начинайте здесь: http://jsbin.com/ayigub/2/edit

Рассмотрим эти почти эквивалентные способы написания простого указателя:

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;
    }
  };
});

При использовании сами по себе две директивы работают и ведут себя одинаково:

  <!-- This works -->
  <div drink-shortcut flavor="blueberry"></div>
  <hr/>

  <!-- This works -->
  <div drink-longhand flavor="strawberry"></div>
  <hr/>

Однако при использовании в ng-repeat работает только версия ярлыка:

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

Мои вопросы:

  • Почему версия longhand не работает внутри ng-repeat?
  • Как вы могли сделать работу с длинной версией внутри ng-repeat?

Ответ 1

В drinkLonghand вы используете код

scope.flavor = attrs.flavor;

Во время фазы связывания интерполированные атрибуты еще не были оценены, поэтому их значения undefined. (Они работают вне ng-repeat, потому что в тех случаях вы не используете строчную интерполяцию, вы просто проходите обычную обычную строку, например "клубнику".) Это упоминается в Руководство разработчика для разработчиков, а также метод Attributes, который отсутствует в документации API называется $observe:

Используйте $observe для наблюдения за изменениями значений атрибутов, которые содержат интерполяцию (например, src="{{bar}}"). Мало того, что это очень эффективно, но это также единственный способ легко получить фактическое значение, потому что во время фазы привязки интерполяция еще не была оценена, и поэтому значение в это время устанавливается на undefined.

Итак, чтобы устранить эту проблему, ваша директива drinkLonghand должна выглядеть так:

app.directive("drinkLonghand", function() {
  return {
    template: '<div>{{flavor}}</div>',
    link: function(scope, element, attrs) {
      attrs.$observe('flavor', function(flavor) {
        scope.flavor = flavor;
      });
    }
  };
});

Однако проблема заключается в том, что он не использует область изоляции; таким образом, линия

scope.flavor = flavor;

имеет возможность перезаписать уже существующую переменную в области с именем flavor. Добавление пустой области выделения также не работает; это потому, что Angular пытается интерполировать строку на основе области действия директивы, на которой нет атрибута flav. (Вы можете проверить это, добавив scope.flav = 'test'; над вызовом attrs.$observe.)

Конечно, вы можете исправить это с помощью определения области выделения, например

scope: { flav: '@flavor' }

или путем создания неизолированной области содержимого

scope: true

или не полагаясь на template на {{flavor}} и вместо этого выполняйте некоторые прямые манипуляции с DOM, такие как

attrs.$observe('flavor', function(flavor) {
  element.text(flavor);
});

но это побеждает цель упражнения (например, было бы проще просто использовать метод drinkShortcut). Итак, чтобы сделать эту директиву, мы вырвем $interpolate сервис, чтобы сделать интерполяцию самостоятельно в области директивы $parent:

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);
    }
  };
});

Конечно, это работает только для начального значения scope.$parent.flav; если значение может измениться, вы должны использовать $watch и переоценить результат интерполяционной функции fn (I ' m не положительно с моей головы, как вы знаете, что делать с $watch, вам просто нужно передать функцию). scope: { flavor: '@' } - хороший ярлык, чтобы избежать необходимости справляться со всей этой сложностью.

[Обновление]

Чтобы ответить на вопрос из комментариев:

Как метод ярлыков решает эту проблему за кулисами? Использует ли это услугу $интерполяции, как и вы, или делает что-то еще?

Я не был уверен в этом, поэтому я посмотрел в источник. Я нашел следующее в 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;
     }

Таким образом, кажется, что attrs.$observe можно внутренне использовать другую область, кроме текущей, чтобы основывать наблюдение атрибута (рядом с последней строкой, над break). Хотя может быть соблазн использовать это самостоятельно, имейте в виду, что что-либо с префиксом с двумя долларами $$ должно считаться приватным для частного API Angular и может быть изменено без предупреждения (не говоря уже о том, что вы получаете это для в любом случае при использовании режима @).