Jan
Jan

Reputation: 8151

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:

Upvotes: 46

Views: 27036

Answers (5)

elpddev
elpddev

Reputation: 4494

I have faced the same problem and ended up adding a little code to ng-transclude so it would allow watch and accept custom data from the using parent.

  • this way the transcluded data have access to the grandparent scope
  • and also have access to the ng-repeat data given to it.

Usage

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>

Code changes to 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);
  }
}

Full code is at Github.

A working example can be seen in Codesandbox.

Upvotes: 0

Morteza Tourani
Morteza Tourani

Reputation: 3536

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>

Upvotes: 9

Jan
Jan

Reputation: 8151

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

Upvotes: 20

Izhaki
Izhaki

Reputation: 23586

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.

Upvotes: 14

Mark Rajcok
Mark Rajcok

Reputation: 364747

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.

Upvotes: 5

Related Questions