9

How to use transclusion in the below case. The intention is to use markup in the html (partials) file, than defining it in template (within the directive).

I found a great tree directive here. (source) Original: http://jsfiddle.net/n8dPm/

Instead of defining the template in the directive, I was trying to use a transcluded content. I also updated Angular to 1.2.0.rc2. Updated: http://jsfiddle.net/aZx7B/2/

got below error

TypeError: Property '$transclude' of object [object Object] is not a function

code:

module.directive("tree", function($compile) {
    return {
        restrict: "E",
        transclude: true,
        scope: {family: '='},
        template:       
            '<ul>' + 
                '<li ng-transclude></li>' +
                '<li ng-repeat="child in family.children">' +
                    '<tree family="child"></tree>' +
                '</li>' +
            '</ul>',
        compile: function(tElement, tAttr) {
            var contents = tElement.contents().remove();
            var compiledContents;
            return function(scope, iElement, iAttr) {
                if(!compiledContents) {
                    compiledContents = $compile(contents);
                }
                compiledContents(scope, function(clone, scope) {
                         iElement.append(clone); 
                });
            };
        }
    };
});

<div ng-app="myapp">
    <div ng-controller="TreeCtrl">
        <tree family="family">
            <p>{{ family.name }}</p>
        </tree>
    </div>
</div>

Edit:

With David's suggestion, made some changes. http://jsfiddle.net/aZx7B/3/ now, it prints, Parent. changing, family -> treeFamily didn't work though

bsr
  • 57,282
  • 86
  • 216
  • 316
  • 1
    A couple issues with this: you're referencing family.name inside the transclusion, but family is part of the directive scope and won't be available. You'd have to use treeFamily.name. Also, your nested trees won't have the transcluded content. You might get further along if you use the transclude function provided to the compile function (3rd parameter) instead of ngTransclude. – David Bennett Oct 01 '13 at 18:44
  • thanks david, update with some changes. – bsr Oct 01 '13 at 18:55
  • I've just been doing something similar and wanted to keep my html in a template. But the recursion wouldn't work (infinite digest - I think) unless I compiled it manually inside the link function. I'd really like to know why this is the case so I can actually make coding decisions based on knowledge and not 'because thats the way it is' – Stevo Oct 03 '13 at 21:49
  • Does my edited answer explain why the template want outputting properly? With my last version of the code you should be able to pass whatever you want to into the your custom directive, inducing other custom directives with templates of their own. – Erstad.Stephen Oct 03 '13 at 22:06

3 Answers3

8

You need to output the name of the family in the template as well: http://jsfiddle.net/roadprophet/DsvX6/

module.directive("tree", function($compile) {
    return {
        restrict: "E",
        transclude: true,
        scope: {family: '='},
        template:       
            '<ul>' + 
                '<li ng-transclude></li>' +
                '<li ng-repeat="child in family.children">' +
                    '<tree family="child">{{family.name}}</tree>' +
                '</li>' +
            '</ul>',
        compile: function(tElement, tAttr, transclude) {
            var contents = tElement.contents().remove();
            var compiledContents;
            return function(scope, iElement, iAttr) {
                if(!compiledContents) {
                    compiledContents = $compile(contents, transclude);
                }
                compiledContents(scope, function(clone, scope) {
                         iElement.append(clone); 
                });
            };
        }
    };
});

EDIT

You could also simplify by doing this: http://jsfiddle.net/roadprophet/DsvX6/2/

<div ng-app="myapp">
    <div ng-controller="TreeCtrl">
        <tree family="treeFamily">           
        </tree>
    </div>
</div>


module.directive("tree", function($compile) {
    return {
        restrict: "E",
        transclude: true,
        scope: {family: '='},
        template:       
            '<ul>' + 
                '<li ng-transclude></li>' +
                '<p>{{ family.name }}</p>' + 
                '<li ng-repeat="child in family.children">' +
                    '<tree family="child"></tree>' +
                '</li>' +
            '</ul>',
        compile: function(tElement, tAttr, transclude) {
            var contents = tElement.contents().remove();
            var compiledContents;
            return function(scope, iElement, iAttr) {
                if(!compiledContents) {
                    compiledContents = $compile(contents, transclude);
                }
                compiledContents(scope, function(clone, scope) {
                         iElement.append(clone); 
                });
            };
        }
    };
});

EDIT Same source of the problem though. No template being passed to the inner tree directive. http://jsfiddle.net/roadprophet/DsvX6/3/

<div ng-app="myapp">
    <div ng-controller="TreeCtrl">
        <tree family="treeFamily">           
                <p>{{ family.name }}</p>
        </tree>
    </div>
</div>

 template:       
            '<ul>' + 
                '<li ng-transclude></li>' +
                '<li ng-repeat="child in family.children">' +
                    '<tree family="child"><div ng-transclude></div></tree>' +
                '</li>' +
            '</ul>'
Erstad.Stephen
  • 1,035
  • 7
  • 9
  • Thanks for the help, but that was not what I needed. I need the `template` to be defined in the html file, and transclude to where I used `ng-transclude`. So, in my example the template in partial was `{{family.name}}`, but it could be much more than that (multi line). – bsr Oct 03 '13 at 19:43
  • 1
    An example of this is in http://stackoverflow.com/questions/14509959/how-do-i-use-transclusion-in-angularjs/14512006#14512006 . In my case, having recursion causing it not to work. The advantage i am trying to get is to have multiple template for the same directive, and easy maintenance (easy to change code in template than JS) – bsr Oct 03 '13 at 19:58
  • Does the new "Edit" make sense? Just reuses the template passed in as the template for the recursive uses of the tree directive. Seem you could then have family structured however you want as far as custom properties go. And the template could be what you want for different uses of tree. – Erstad.Stephen Oct 03 '13 at 20:07
  • This seems to meet your needs/requirements. Sorry about the initial misunderstanding. – Erstad.Stephen Oct 03 '13 at 20:28
  • I can also give a more thorough explanation of why this is if it does make sense. – Erstad.Stephen Oct 03 '13 at 22:07
  • 1
    Stephen. Great, I guess that exactly I was looking for. Let me do some more testing, and get back with you. Thanks – bsr Oct 03 '13 at 22:08
  • let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/38580/discussion-between-bsr-and-erstad-stephen) – bsr Oct 03 '13 at 22:17
  • Just wanted to mention that this is not the expected behavior for transclusion in angular. I don't think that the transcluded HTML has access to parent controller scope this way. – webaba Nov 04 '14 at 15:37
1

You want to compile the transcluded DOM against the parent scope; you can do this automatically with the injectable $transclude function in a directive's controller definition:

module.directive("tree", function($compile) {
  return {
    restrict: "E",
    transclude: true,
    scope: { family: '=' },
    template: '<ul>' + 
                '<li ng-repeat="child in family.children">' +
                  '<tree family="child">' +
                    '<p>{{ child.name }}</p>' +
                  '</tree>' +
                '</li>' +
              '</ul>',
    controller: function($element, $transclude) {
      $transclude(function(e) {
        $element.append(e);
      });
    },
    compile: function(tElement, tAttr, transclude) {
      var contents = tElement.contents().remove();
      var compiledContents;
      return function(scope, iElement, iAttr) {
        if(!compiledContents) {
          compiledContents = $compile(contents);
        }
        compiledContents(scope, function(clone) {
          iElement.append(clone);
        });
      };
    }
  };
});

This allows you to use the parent scope property treeFamily in your root template (also notice the use of child in the directive's template, above):

<div ng-app="myapp">
  <div ng-controller="TreeCtrl">
    <tree family="treeFamily">
      <p>{{ treeFamily.name }}</p>
    </tree>
  </div>
</div>

You can see an example here: http://jsfiddle.net/BinaryMuse/UzHeW/

Michelle Tilley
  • 157,729
  • 40
  • 374
  • 311
  • 1
    Brandon, thanks. But, it is '

    {{ child.name }}

    ' is outputting the tree name (which is part of directive). That defeats the purpose. I actually want that part in the html. This is a simple case, but I may add more markup in html. I just need access to the tree `node` in html, and render the markup defined in html against this. So, I can reuse the directive, but easily change the included markup in html. I thought that was one of the purpose of transclusion. Anyway, If I remove the markup from directive, nothing outputs.http://jsfiddle.net/UzHeW/8/ Please let me know if I am not clear yet.
    – bsr Oct 03 '13 at 22:06
0

Very very late to the party. I needed this for a project so after dwelling into it and finding other great approaches and directions, finally came up with this:

Same code as the ng-transclude directive, but with small addition of context binding that the directive watch and sets on the transcluded generated scope each time it changes. Same as ng-repeat does, but this allows:

  1. using the customize ng-transclude with ng-repeat without the hassle of rewriting ng-repeat and like angular/2 template outlet.
  2. Having the transcluded content keep access the grandparent scope while receiving direct context data from the parent. Same as template outlet again.

The augmented ng-transclude function:

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

And it's usage:

App

<my-list items="$ctrl.movies">
   <div>App data: {{ $ctrl.header }}</div>
   <div>Name:{{ name }} Year: {{ year }} Rating: {{ rating 
          }}</div>
</my-list>

MyList

<ul>
  <li ng-repeat="item in $ctrl.items track by item.id">
   <div>Ng repeat item scope id: {{ $id }}</div>
   <cr-transclude context="item"></cr-transclude>
  </li>
</ul>

Full directive code can be found here on GitHub

elpddev
  • 4,314
  • 4
  • 26
  • 47