【问题标题】:ng-repeat with ng-transclude inside a directive在指令中使用 ng-transclude 进行 ng-repeat
【发布时间】:2025-12-21 07:45:12
【问题描述】:

我想在内容更改时创建一个具有自定义行为的列表。我尝试为此创建一个指令,但我对如何将 ng-transclude 与 ng-repeat 指令结合起来有点迷茫。有人能让我走上正轨吗?

HTML:

<div ng-app="myApp">
  <div ng-controller="ctrl">
    <mylist items="myItem in items">
       <span class="etc">{{myItem}}</span>
    </mylist>
  </div>
</div>

Javascript:

angular.module('myApp', [])    

.controller('ctrl', function ($scope) {
  $scope.items = ['one', 'two', 'three'];
})    

.directive('mylist', function () {
  return {
    restrict:'E',
    transclude: 'element',
    replace: true,
    scope: true,
    template: [
      '<ul>',
        '<li ng-repeat="WhatGoesHere in items" ng-transclude></li>',
      '</ul>'
    ].join(''),
    link: function (scope, element, attr) {
      var parts = attr.items.split(' in ');
      var itemPart = parts[0];
      var itemsPart = parts[1];
      scope.$watch(itemsPart, function (value) {
        scope.items = value; 
      });      
    }
  }
});

herehere

编辑:

标准:

  • 项目的模板必须在视图中定义,而不是在指令中,并且它必须能够访问子范围内的项目属性。理想情况下,我想像在 ng-repeat 指令中那样定义它
  • 指令必须有权访问列表,这样我才能设置正确的监视和更改内容。如果可能的话,我希望能够轻松访问生成的 DOM 项(我也可以使用 element[0].querySelectorAll('ul&gt;li') 或其他东西来实现,它只需要在 Chrome 上工作)。
  • 如果可能的话,我想重用 ng-repeat 指令中的逻辑,因为它已经做了很多我想做的事情。最好我不想复制代码。我只是想增强它的行为,而不是改变它

【问题讨论】:

    标签: javascript angularjs


    【解决方案1】:

    我遇到了同样的问题,最后在ng-transclude 中添加了一些代码,这样就可以观察和接受来自使用父级的自定义数据。

    • 这样转入的数据可以访问祖父范围
    • 并且还可以访问提供给它的 ng-repeat 数据。

    用法

    Grandparent.js

    <div>{{ $ctrl.listName }}</div.
    
    <my-list items="$ctrl.movies">
       <div>From context: {{ name }}</div>
       <div>From grandparent: {{ $ctrl.listName }}</div>
    </my-list>
    

    Parent.js (MyList)

    <li ng-repeat="item in $ctrl.items track by item.id">
      <cr-transclude context="item"></cr-transclude>
    </li>
    

    ng-transclude 的代码更改

    return function ngTranscludePostLink(
       ...
      ) {
      let context = null;
      let childScope = null;
      ...
      $scope.$watch($attrs.context, (newVal, oldVal) => {
        context = newVal;
        updateScope(childScope, context);
      });
      ...
      $transclude(ngTranscludeCloneAttachFn, null, slotName);
      ...
      function ngTranscludeCloneAttachFn(clone, transcludedScope) {
         ...                                 
         $element.append(clone);
         childScope = transcludedScope;
         updateScope(childScope, context);
         ...
      }
      ...
      function updateScope(scope, varsHash) {
        if (!scope || !varsHash) {
          return;
        }
        angular.extend(scope, varsHash);
      }
    }
    

    完整代码在Github

    可以在Codesandbox 中看到一个工作示例。

    【讨论】:

      【解决方案2】:

      其他答案不幸的是不适用于最新版本的 angular(我检查了1.4)所以我认为分享this jsbin 有好处我发现:

      var app = angular.module('app', [])
        .controller('TestCtrl', function($scope) {
          $scope.myRecords = ['foo', 'bar', 'baz'];
        });
      
      app.directive('myDirective', function($compile) {
        var template = '<div id="inner-transclude" ng-repeat="record in records"></div>';
      
        return {
          scope: {
            records: '='
          },
          restrict: 'A',
          compile: function(ele) {
            var transclude = ele.html();
            ele.html('');
      
            return function(scope, elem) {
              var tpl = angular.element(template);
              tpl.append(transclude);
      
              $compile(tpl)(scope);
      
              elem.append(tpl);
            };
          }
        };
      });
      <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.0/angular.js"></script>
      
      
      <div ng-app="app" ng-controller="TestCtrl">
        <div my-directive records="myRecords">
          ?: {{record}}
        </div>
      
      </div>

      【讨论】:

      • 这对我来说效果很好,也很简单......谢谢!
      • 我不得不做一个whole lot of additional work 但这个答案看起来基本上对我有用。
      【解决方案3】:

      自己解决了问题:

      我可以在编译步骤 (jsfiddle) 中执行此操作,方法是在编译模板时添加 ng-repeat 属性并将我的属性内容提供给它。

      HTML:

      <div ng-app="myApp">
        <div ng-controller="ctrl">
          <mylist element="myItem in items">{{myItem}}</mylist>
        </div>
      </div>
      

      Javascript:

      var myApp = angular.module('myApp', [])
      
      .controller('ctrl', function ($scope) {
        $scope.items = ['one', 'two', 'three'];
      })
      
      .directive('mylist', function ($parse) {
        return {
          restrict:'E',
          transclude: 'element',
          replace: true,
          scope: true,
          template: [
            '<ul>',
            '<li ng-transclude></li>',
            '</ul>'
          ].join(''),
          compile: function (tElement, tAttrs, transclude) {
            var rpt = document.createAttribute('ng-repeat');
            rpt.nodeValue = tAttrs.element;
            tElement[0].children[0].attributes.setNamedItem(rpt);
            return function (scope, element, attr) {
              var rhs = attr.element.split(' in ')[1];
              scope.items = $parse(rhs)(scope);
              console.log(scope.items);
            }        
          }
        }
      });
      

      【讨论】:

      • 这太棒了!有一件事对我不起作用:在编译的返回函数中,$parse(rhs)(scope) 总是返回undefined,但如果我执行$parse(rhs)(scope.$parent),我可以找到正确的变量。你的为什么在子作用域上起作用——items 不是在父作用域中声明的吗?
      • items 在父作用域中声明。您是否使用隔离范围? scope: {} 而不是 scope: true
      • 这在当前的 Angular 版本中不再起作用。请参阅此讨论:github.com/angular/angular.js/issues/7874
      • @ulilicht 我认为this 会是答案
      【解决方案4】:

      实现此目的的另一种方法如下。

      索引.html:

      <html ng-app='myApp'>
      
      <head>
          <title>AngularJS Transclude within Repeat Within Directive</title>
          <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.18/angular.min.js"></script>
          <script src='index.js'></script>
      </head>
      
      <body ng-controller='myController'>
          <people>Hello {{person.name}}</people>
          <button name="button" ng-click="changeRob()">Change Rob</button>
      </body>
      </html>
      

      index.js:

      var myApp = angular.module( 'myApp', [] );
      
      myApp.controller( 'myController', function( $scope ) {
          $scope.people = [
              { name: 'Rob'  },
              { name: 'Alex' },
              { name: 'John' }
          ];
      
          $scope.changeRob = function() {
              $scope.people[0].name = 'Lowe';
          }
      });
      
      myApp.directive( 'people', function() {
          return {
              restrict: 'E',
      
              transclude: true,
              template: '<div ng-repeat="person in people" transcope></div>',
          }
      });
      
      myApp.directive( 'transcope', function() {
          return {
              link: function( $scope, $element, $attrs, controller, $transclude ) {
                  if ( !$transclude ) {
                      throw minErr( 'ngTransclude' )( 'orphan',
                          'Illegal use of ngTransclude directive in the template! ' +
                          'No parent directive that requires a transclusion found. ' +
                          'Element: {0}',
                          startingTag( $element ));
                  }
                  var innerScope = $scope.$new();
      
                  $transclude( innerScope, function( clone ) {
                      $element.empty();
                      $element.append( clone );
                      $element.on( '$destroy', function() {
                          innerScope.$destroy();
                      });
                  });
              }
          };
      }); 
      

      similar plunker 中查看它的实际应用。基于此long Github issue discussion

      【讨论】:

        【解决方案5】:

        Transcluding 不是必需的,因为items 包含我们渲染模板所需的内容。换句话说,元素内部没有任何东西——即&lt;mylist&gt;nothing new here we need to transclude&lt;/mylist&gt;。看来 Angular 也会为我们做 $watching。

        .directive('mylist', function () {
          return {
            restrict:'E',
            replace: true,
            scope: true,
            template: [
              '<ul>',
              '<li ng-repeat="myItem in items">{{myItem}}</li>',
              '</ul>'
            ].join('')
          }
        });
        

        HTML:

        <mylist></mylist>
        

        Fiddle.

        请注意,创建新范围是可选的,因此您可以注释掉这一行:

        //scope: true,
        

        更新:您可以选择创建隔离范围:

        scope: { items: '='},
        

        HTML:

        <mylist items=items></mylist>
        

        Fiddle.

        Update2:基于 Jan 提供的其他信息:

        项目的模板必须在视图中定义...我想重用ng-repeat指令中的逻辑

        好的,让我们把它全部放在视图中,并使用 ng-repeat:

        <ul mylist>
          <li ng-repeat="myItem in items">
            <span class="etc">{{myItem}}</span>
           </li>
        </ul>
        

        它 [指令] 必须有权访问子范围内的项目属性...该指令必须有权访问列表,以便我可以设置适当的监视和更改内容

        按照你原来的小提琴,我们将使用一个普通的子范围(即,子范围将原型继承自父范围):scope: true,。这将确保指令可以访问在控制器范围内定义的属性,例如 items

        访问生成的 DOM 项

        指令的链接函数有一个element 参数。所以在上面的 HTML 中,元素将被设置为 &lt;ul&gt; 元素。所以我们可以访问所有的 DOM 元素。例如,element.find('li')element.children()。在下面引用的小提琴中,我有它 $watch items 数组。 $watch 回调可以访问element,因此您可以访问生成的 DOM 项。回调将element.children() 记录到控制台。

        Fiddle.

        总而言之,要将自定义行为添加到列表中,只需将指令放在 ul 或 ol 上即可。

        【讨论】:

        • 你不懂,我在&lt;mylist&gt;&lt;/mylist&gt;里面的item模板比我这里给出的例子复杂多了
        • 里面还有什么——只是文本,还是其他 $scope 属性?
        • 你看,我在现实生活中的列表是一个包含大对象的列表,其中的项目包含很多范围属性。我只是简化了我的例子。我想要一个指令,它在具有隔离范围的 ul>li 结构上充当 ng-repeat,因此我可以查看 items 属性并在列表更改时执行列表中的操作。喜欢,加载更多项目,更改滚动行为等
        • 你能把你需要的东西放入指令的模板中,而不是嵌入吗?如果没有,我需要看一个你试图嵌入的例子,以进一步帮助你。此外,您可以使用 $watch 观察整个 items 属性,但将第三个参数设置为 true 以进行“深度检查”/对象相等性,而不仅仅是引用检查:scope.$watch('items', function(newVal) { ... }, true);
        • 我可以将视图逻辑放在指令中,也可以将 DOM 操作放在控制器中,这都是不好的做法。我需要一个模仿 ng-repeat 指令的指令,但我想添加其他行为。我的用例在这里:github.com/Janpotoms/google-reader-notifier/blob/master/app/… 在里面查找#grn-items-list。但这没关系。我想要一个可重用的组件