在ng-repeat中使用指令,以及范围'@'的神秘力量

Jon*_*nah 60 javascript angularjs angularjs-directive

如果您希望在工作代码中看到问题,请从这里开始: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;
    }
  };
});
Run Code Online (Sandbox Code Playgroud)

当它们自己使用时,这两个指令的工作和行为相同:

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

  <!-- This works -->
  <div drink-longhand flavor="strawberry"></div>
  <hr/>
Run Code Online (Sandbox Code Playgroud)

但是,在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>
Run Code Online (Sandbox Code Playgroud)

我的问题是:

  1. 为什么缩写版本在ng-repeat中不起作用?
  2. 你怎么能在ng-repeat中使用longhand版本?

Mic*_*ley 101

drinkLonghand,您使用代码

scope.flavor = attrs.flavor;
Run Code Online (Sandbox Code Playgroud)

在链接阶段,尚未评估插值属性,因此它们的值是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;
      });
    }
  };
});
Run Code Online (Sandbox Code Playgroud)

但是,问题在于它不使用隔离范围; 因此,这条线

scope.flavor = flavor;
Run Code Online (Sandbox Code Playgroud)

有可能覆盖名为范围的预先存在的变量flavor.添加空白隔离范围也不起作用; 这是因为Angular尝试根据指令的范围插入字符串,在该范围内没有调用属性flav.(您可以通过scope.flav = 'test';在调用上方添加来测试attrs.$observe.)

当然,您可以使用隔离范围定义来解决此问题

scope: { flav: '@flavor' }
Run Code Online (Sandbox Code Playgroud)

或者通过创建一个非隔离的子范围

scope: true
Run Code Online (Sandbox Code Playgroud)

或者不依赖于templatewith {{flavor}}而是做一些直接的DOM操作之类的

attrs.$observe('flavor', function(flavor) {
  element.text(flavor);
});
Run Code Online (Sandbox Code Playgroud)

但这违背了练习的目的(例如,使用该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);
    }
  };
});
Run Code Online (Sandbox Code Playgroud)

当然,这仅适用于初始值scope.$parent.flav; 如果值能够改变,你必须使用$watch并重新评估插值函数的结果fn(我不知道你怎么知道该怎么做$watch;你可能只需要传入一个功能).scope: { flavor: '@' }是避免必须管理所有这些复杂性的一个很好的捷径.

[更新]

要回答评论中的问题:

如何在幕后解决这个问题的快捷方法?它是否像你一样使用$ interpolate服务,还是在做其他事情?

我不确定这一点,所以我查看了源代码.我发现以下内容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;
     }
Run Code Online (Sandbox Code Playgroud)

因此,似乎attrs.$observe可以在内部告知使用与当前范围不同的范围来基于属性观察(在最后一行的上一行,在其上方break).虽然自己使用它可能很诱人,但请记住,带有双美元$$前缀的任何内容都应该被视为Angular私有API的私有,并且可能会在没有警告的情况下进行更改(更不用说在使用时无论如何都可以免费获得)@模式).