ng-transclude in ng-repeat正在失去对$ transclude函数的访问权限

时间:2014-03-04 02:48:57

标签: angularjs angularjs-directive

我有一个类似列表的容器指令,可以将内容转换为ng-repeat。

模板如下所示:

<div ng-repeat='item in items'>
    <div ng-transclude></div>
</div>

,用法如下所示

<my-container>
    foo
</my-container>

这可以按预期工作。但是,只要我的模板中的ng-repeat上面有任何指令,例如

<div ng-style='{}'>
    <div ng-repeat='item in items'>
        <div ng-transclude></div>
    </div>
</div>

我的代码抛出

"Illegal use of ngTransclude directive in the template! No parent directive that requires a transclusion found. Element: <div ng-transclude="">"

首先看一下Angular代码,看起来Angular没有正确地将$ transclude注入设置到ngTransclude控制器中。我将开始挖掘Angular代码,试图找出原因,但如果有人已经知道发生了什么和/或如何解决或解决它,我将非常感激。

以下是积极案例的全功能小提琴:http://jsfiddle.net/7BuNj/1/

对于否定案例,这是一个完全无功能的小提琴:http://jsfiddle.net/8BLYG/

1 个答案:

答案 0 :(得分:7)

tl; dr tl; dr tl; dr

看起来Angular递归编译循环不会通过带有链接函数的指令将转换函数断开。我的ngStyle指令有一个链接函数,因此在ngRepeat编译其模板时,转换功能就丢失了。目前尚不清楚这是预期的行为还是一个错误;我稍后会与Angular团队进行跟进。现在,我暂时修补了Angular v.1.2.0的副本,用

代替5593-5594行。
: compileNodes(childNodes,
  (nodeLinkFn && nodeLinkFn.transclude) ? nodeLinkFn.transclude : transcludeFn);

一切正常。

研究

好的。为了使这对非Angular专家(例如,我:)有任何意义,我将对Angular的编译/链接循环的代码进行一些了解。我在这里提供了v1.2.0相关位的超级精简版本供参考(对不起Angular团队,我打破了我想象的相当宗教风格的指导方针,试图让代码尽可能短的代码段;) :

function compileNodes(nodeList, transcludeFn, $rootElement, maxPriority, ignoreDirective, previousCompileContext) {
  ...
  for (var i = 0; i < nodeList.length; i++) {
    ...
    nodeLinkFn = (directives.length)
                 ? applyDirectivesToNode(directives, nodeList[i], attrs, transcludeFn, $rootElement, null, [], [], previousCompileContext)
                 : null;
    ...
    childLinkFn = (nodeLinkFn && nodeLinkFn.terminal || !(childNodes = nodeList[i].childNodes) || !childNodes.length)
                  ? null
                  : compileNodes(childNodes, nodeLinkFn ? nodeLinkFn.transclude : transcludeFn);
    ...
    linkFns.push(nodeLinkFn, childLinkFn);
    ...
  }
  return linkFnFound ? compositeLinkFn : null;

  function compositeLinkFn(scope, nodeList, $rootElement, boundTranscludeFn) {
    ...
    for(i = 0, n = 0, ii = linkFns.length; i < ii; n++) {
      ...
      nodeLinkFn = linkFns[i++];
      childLinkFn = linkFns[i++];
      if (nodeLinkFn) {
        ...
        childTranscludeFn = nodeLinkFn.transclude;
        if (childTranscludeFn || (!boundTranscludeFn && transcludeFn)) {
          nodeLinkFn(childLinkFn, childScope, node, $rootElement, createBoundTranscludeFn(scope, childTranscludeFn || transcludeFn));
        } else {
          nodeLinkFn(childLinkFn, childScope, node, $rootElement, boundTranscludeFn);
        }
      } else if (childLinkFn) {
        childLinkFn(scope, node.childNodes, undefined, boundTranscludeFn);
      }
    }
  }
}

function applyDirectivesToNode(directives, compileNode, templateAttrs, transcludeFn, jqCollection, originalReplaceDirective, preLinkFns, postLinkFns, previousCompileContext) {
  ...
  if (directiveValue = directive.transclude) {
    hasTranscludeDirective = true;
    if (directiveValue == 'element') {
      ...
      $template = groupScan(compileNode, attrStart, attrEnd);
      ...
      childTranscludeFn = compile($template, transcludeFn, terminalPriority, replaceDirective && replaceDirective.name, { ... });
    } else {
      $template = jqLite(jqLiteClone(compileNode)).contents();
      $compileNode.empty(); // clear contents
      childTranscludeFn = compile($template, transcludeFn);
    }
  }
  ...
  // setup preLinkFns and postLinkFns
  ...
  nodeLinkFn.transclude = hasTranscludeDirective && childTranscludeFn;
  return nodeLinkFn;

  function nodeLinkFn(childLinkFn, scope, linkNode, $rootElement, boundTranscludeFn) {
    ...
    forEach(controllerDirectives, function(directive) {
      ...
      transcludeFn = boundTranscludeFn && controllersBoundTransclude;
      // puts transcludeFn into controller locals for $transclude access
      ...
    }
    ...
    // PRELINKING: call all preLinkFns with boundTranscludeFn
    ...
    // RECURSION
    ...
    childLinkFn && childLinkFn(scopeToChild, linkNode.childNodes, undefined, boundTranscludeFn);
    ...
    // POSTLINKING: call all preLinkFns with boundTranscludeFn
    ... 
  }
}

无翻译编译/链接

基本上,编译周期通过compileNodes对DOM进行深度优先搜索。在每个节点,

  1. 它通过applyDirectivesToNode将该节点上的任何指令编译成nodeLinkFn,该nodeLinkFn可以访问任何已配置的prelink或postlink函数,并接受子compositeLinkFn进行递归
  2. 以递归方式将节点的子树编译为compositeLinkFn
  3. 将nodeLinkFn与子树compositeLinkFn一起打包到节点级compositeLinkFn中(跳过nodeLinkFn,如果为空)
  4. 编译完成后,你有一个顶层的compositeLinkFn,它的执行深度优先与编译过程完全并行,并且在每个节点执行所有的预链接函数,递归,然后执行所有的postlink函数。

    编译/链接1级转换

    每当applyDirectivesToNode命中设置了transclude标志的指令时,它就会将指令元素的内容完全编译成与当前编译递归分开的“transclude链接函数”,并用它来注释指令的nodeLinkFn。当封闭的compositeLinkFn最终被执行时,这个注释被读取并作为boundTranscludeFn传递给nodeLinkFn,最终它将被设置为controller $ transclude注入,传递给所有prelink和postlink函数,并传递给子链接的递归调用功能。 boundTranscludeFn的这种递归线程是关键,因此您可以在transcluding指令模板中的任何位置访问$ transclude。

    编译/链接多个级别的转换

    现在如果我们在transcluding指令A中转换指令B怎么办?我们想要发生的事情(我假设)是指令A的transclude函数以某种方式提供给指令B的转换内容。毕竟,B的转换内容应该是A的模板中的所有意图和目的。问题是B的transclude内容是从A的编译递归分别编译到它自己的tranclude链接函数中的,它不会是A的链接递归的一部分,因此在链接时不会收到A的translude函数。

    Angular通过以下方式解决了这个问题:

    1. 另外通过compileNodes递归线程转换函数(不仅仅是链接递归)
    2. 在编译时将transcludeFn直接传递到transclude内容编译周期
    3. 允许compositeLinkFn从其封闭的compileNodes调用中拉出transclude函数
    4. 结论

      我在原始问题中遇到的问题是通过编译递归来进行翻译功能的线程化。链接递归始终正确地将转换函数传递给子链接函数,但是如果当前节点的指令没有链接函数,则编译递归仅向下传递转置函数,无论该指令是否转换:

      compileNodes(childNodes, nodeLinkFn ? nodeLinkFn.transclude : transcludeFn);
      

      我的问题爆炸是因为ngStyle创建了一个链接函数,但没有转换。所以突然之间,transclude函数停止在编译循环中进行操作,在ngRepeat编译其子内容时可以使用它。 “修复”是将违规行更改为:

      compileNodes(childNodes, (nodeLinkFn && nodeLinkFn.transclude) ? nodeLinkFn.transclude : transcludeFn);
      

      基本上,现在只有当一个新的,更深层嵌套的transclude函数替换它时,它才会停止线程化transclude函数(这与Angular文档一致,即内容应该被转换为最近的transcluding parent指令)。同样,我不确定所有这些都是预期的行为或错误,但希望无论如何它都是有用的转换? :)