ng-repeat with ng-transclude inside a directive

I want to create a list with custom behavior when it's content changes. I try to create a directive for this but I get a bit lost with how to combine the ng-transclude with the ng-repeat directive. Can somebody put me on track?

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

I've got part of this somewhat working here

EDIT:

Criteria:

  • The template of the item must be defined in the view, not in the directive and it must have access to an item property in a child scope. Ideally I want to define this like it is done in the ng-repeat directive
  • The directive must have access to the list so I can set proper watches and change things. If possible I would like to have easy access to the generated DOM items (I can also do it with element[0].querySelectorAll('ul>li') or something, It only has to work on Chrome).
  • If possible I would like to reuse the logic in the ng-repeat directive because it does already do a lot of what I want. Preferably I don't want to copy the code. I just want to augment its behavior, not change it

Answers:

Answer

Solved the problem myself:

I am able to do it in the compile step (jsfiddle) by adding the ng-repeat attribute when the template is compiled and feeding it the content of my attribute.

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

An alternative way to achieve this as follows.

Index.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();
                });
            });
        }
    };
}); 

See it in action in this similar plunker. Based on this long Github issue discussion.

Answer

Other answers unfortunately does not work with newest version of angular(I checked 1.4) so I think there is a benefit to share this jsbin I found:

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>

Answer

Transcluding isn't necessary because items contains what we need to render the template. Put another way, there isn't anything inside the element -- i.e., <mylist>nothing new here we need to transclude</mylist>. It seems Angular will do the $watching for us too.

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

Note that creating a new scope is optional, so you could comment out this line:

//scope: true,

Update: You could optionally create an isolate scope:

scope: { items: '='},

HTML:

<mylist items=items></mylist>

Fiddle.

Update2: based on additional info provided by Jan:

The template of the item must be defined in the view... I would like to reuse the logic in the ng-repeat directive

Okay, so lets put it all in the view, and use ng-repeat:

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

it [the directive] must have access to an item property in a child scope... The directive must have access to the list so I can set proper watches and change things

Following your original fiddle, we'll use a normal child scope (i.e., the child scope will prototypically inherit from the parent scope): scope: true,. This will ensure the directive has access to the properties defined on the controller's scope, e.g., items.

access to the generated DOM items

The directive's link function has an element argument. So in the HTML above, element will be set to the <ul> element. So we have access to all the DOM elements. E.g., element.find('li') or element.children(). In the fiddle referenced below, I have it $watch the items array. The $watch callback has access to element, so you have access to the generated DOM items. The callback logs element.children() to the console.

Fiddle.

In summary, to add custom behavior to a list, just plop a directive onto a ul or ol and away you go.

Tags

Recent Questions

Top Questions

Home Tags Terms of Service Privacy Policy DMCA Contact Us

©2020 All rights reserved.