Angular.js: angular 1.2.18: ng-repeat problem with transclude

Created on 17 Jun 2014  ·  48Comments  ·  Source: angular/angular.js

When pass to a directive by ng-transclude, html content with reference {{item}} that you want to repeat (through ng-repeat="item in collection" implemented in the directive) does not work with version 1.2.18

http://embed.plnkr.co/EvzF25sPD3uZLQivDFqy/preview

Most helpful comment

What I ended up doing was just using $parent. It's the closet to vanilla without having to add too many things.

So I have something like:

angular.module('test').directive('myDirectiveWithTransclusion', function() {
     return {
          restrict: 'E'
          transclude: {
               transcludeThis: 'transcludeThis'
          }
          template: "<div ng-repeat='item in array'><div ng-transclude='transcludeThis'></div></div>"
     }
})
<my-directive-with-transclusion>
     <transclude-this>
          {{$parent.item}}
     </transclude-this>
</my-directive-with-transclusion>

All 48 comments

oh lawdy. @petebacondarwin want to do another one of these? this is really the "don't create a sibling scope with ng-transclude" thing again, it's just that it worked before for this case due to brokenness

Sadly, this is not how ng-transclude transclusion works. What you are trying to do is create a container directive that makes use of its children as a "template" of what to stamp out inside your own directive.

I ran into this a few times in the past and had a long debate with Misko about it. The content that is transcluded is by definition bound to the scope of the place where the directive is instantiated; not to the scope of the directive's template.

Previously this may have worked as we were actually binding the transclusion to the wrong scope in some cases that involved nested transclusion scenarios.

So actually you don't really need to use transclusion here as what you really trying to do is simply inject the inner HTML into your own template. You can do this in the compile function like this:

http://plnkr.co/edit/j3NwMGxkVRM6QMhmydQC?p=preview

app.directive('test', function(){

  return {
    restrict: 'E',
    compile: function compile(tElement, tAttrs, tTransclude) {

      // Extract the children from this instance of the directive
      var children = tElement.children();

      // Wrap the chidren in our template
      var template = angular.element('<div ng-repeat="item in collection"></div>');
      template.append(children);

      // Append this new template to our compile element
      tElement.html('');
      tElement.append(template);

      return {
        pre: function preLink(scope, iElement, iAttrs, crtl, transclude) {
            scope.collection = [1, 2, 3, 4, 5];
        },
        post: function postLink(scope, iElement, iAttrs, controller) {
          console.log(iElement[0]);
        }
      };
    }
  };
});

Another example of this (I believe):

Index.html:

<html ng-app='myApp'>

<head>
    <title>AngularJS Scopes</title>
    <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.1/angular.min.js"></script>
    <script src='index.js'></script>
</head>

<body ng-controller='myController'>
    <people>Hello {{person.name}}</people>
</body>
</html>

index.js:

var myApp = angular.module( 'myApp', [] );

myApp.controller( 'myController', function( $scope ) {
    $scope.people = [
        { name: 'Rob'  },
        { name: 'Alex' },
        { name: 'John' }
    ];
});

myApp.directive( 'people', function() {
    return {
        restrict: 'E',

        transclude: true,
        template: '<div ng-repeat="person in people" ng-transclude></div>',
    }
});

Worked with Angular 1.2.1, but not with 1.2.18;

The innocent developer could only expect the code above to work. This doc says:

...sometimes it's desirable to be able to pass in an entire template rather than a string or an object. Let's say that we want to create a "dialog box" component. The dialog box should be able to wrap any arbitrary content.

While the ngTransclude documentation says:

Directive that marks the insertion point for the transcluded DOM of the nearest parent directive that uses transclusion. Any existing content of the element that this directive is placed on will be removed before the transcluded content is inserted.

How does this differ from @petebacondarwin definition:

...create a container directive that makes use of its children as a "template" of what to stamp out inside your own directive

I really don't understand why transclusion isn't the right solution here. If anything I would expect the reasonable solution to involve injecting the template scope to the transclusion function.

The difference is that transcluded content is bound to the "outside", i.e. the scope of place where the <people> element is found.
Whereas what you want is for the inline template to be bound to the "inside", i.e. the scope of the directive.
If your directive does not create its own scope, then this is the approximately same thing. If your directive creates an isolate scope, say, then it definitely is not the same thing. The inner scope has no access to the outer scope.

I guess you could create your own directive which does indeed inject the template scope into the transclusion function...

What you are saying makes perfect sense - but that is only if you understand the depths of Angular. My point is that the example above should somehow work without too much extra work.

Seems to me very reasonable, and highly practical for the transclusion function to be able to access to the template (or the 'inside') scope somehow. I can think of many cases why this will be needed.

The very same issue is explained in this blog. My reckoning that more and more people will complain about how things are at the moment (there's already a multitude of related issues on Github as a result of this behaviour).

And thanks very much for the code. I'm replicating it here for the benefit of others:

var myApp = angular.module( 'myApp', [] );

myApp.controller( 'myController', function( $scope ) {
    $scope.people = [
        { name: 'Rob'  },
        { name: 'Alex' },
        { name: 'John' }
    ];
});

myApp.directive( 'people', function() {
    return {
        restrict: 'E',
        transclude: true,
        template: '<div ng-repeat="person in people" inject></div>'
    }
});

myApp.directive('inject', 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();
        });
      });
    }
  };
});

:-) I agree that transclusion is not an easy topic to understand without a lot of head scratching and keyboard banging.
Perhaps we need to clarify the documentation even further around the fact that ng-transclude will bind the transcluded content to the "outside" scope?

Personally, I feel the current documentation, at least on this pivotal page, is fairly explicit and clear:

What does this transclude option do, exactly? transclude makes the contents of a directive with this option have access to the scope outside of the directive rather than inside.

(and everything that follows illustrates this further).

I would consider adding perhaps the directive you have provided to the framework, possibly branding it 'ng-transclude-internal'? I know of at least one more person who had a go at this, with the directive called 'transcope'.

I have tried solution, but I faced with other problem, why into ng-repeat parent scope is not scope of directive

http://plnkr.co/edit/7j92IC?p=preview

@luboid the reason that doesn't work in your plunker is because your directive has an isolate scope, and the modified compiled DOM (don't do this, by the way, this is a silly way to solve this problem) will use a sibling of the isolate scope's parent.

I'll add an example of a proper way to make that work how you expect. (But, this is still a pretty awful design in general, there's no good reason to do this)

Actually, come to think of it, with ng-repeat or other element transclusion directives you can't really fix this. So yeah, that won't work ever since about version 1.2.0

@petebacondarwin A really newbie question here. Why use var innerScope instead of just:

myApp.directive( 'inject', 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 ));
            }

            $transclude( $scope, function( clone ) {
                $element.empty();
                $element.append( clone );
            });
        }
    };
}); 

It is generally best to create a new scope if you are going to compile some new elements, since you are not sure if the original directive is collocated with some complex directive that also wants to create scope, etc. But you might be able to get away with it here...

Thanks for help,
I will use external templates ($templateCache), then things going little simple, base template is compiled with directive scope

@Izhaki @petebacondarwin Thanks so much for that include directive. Just finally updated from 1.2.16 to 1.2.20 and it took be a bit to see why my app started breaking so hard.

I thought transclusion was a pretty simple concept before I found this thread. Nope.

Thanks to everyone who's been helping to clear this up. Updating to 1.2.21 broke much of our interface because of the issues described here, and now we're on the right track.

@caitp @petebacondarwin: It would be very helpful if we could get two additional pieces of information:

  1. Which breaking change that happened somewhere "about version 1.2.0" (as Caitlin said) resulted in this approach suddenly exploding? I understand what doesn't work any more, but I don't see any specific thing in the changelog near that release that seems to correlate to this particular issue.
  2. What's the alternate approach end-users should employ in lieu of ng-transclude if we want isolated, reusable containers from which arbitrary inner content can inherit? Example: a large, multi-tenant application where even the interface is variable and totally data-driven. So, we've got things like list controls that need to go retrieve data to populate themselves, then have consistent interaction/ behavior. We'd like to be able to standardize a wrapper directive that provides those things. But the templates we use for the list items (which may contain inner directives) vary depending on the situation. And the lists are often nested, recursively, based on the tree-structure of the data that generates them. So, we need isolation for those nested lists. We do control all the code, so we're not using transclusion in order to isolate inner from outer. Rather, we're just after a nice, declarative approach to the containers and their arbitrary content, as explicitly recommended in the Directive Guide. Is ng-include the better option for this now (passing the path to the inner template as an attribute on the container declaration), even though it gets us away from inline templates, and thus feels a bit less idiomatic?

I understand what doesn't work any more, but I don't see any specific thing in the changelog near that release that seems to correlate to this particular issue.

https://github.com/angular/angular.js/blob/master/CHANGELOG.md#120-timely-delivery-2013-11-08

I was referring to these in particular

  • only pass isolate scope to children that belong to the isolate directive (d0efd5ee)
  • make isolate scope truly isolate (909cabd3, #1924, #2500)

But, I can't really remember what I was even talking about anymore, who knows ヽ༼ຈل͜ຈ༽ノ

In the changelog, there are only two kinds of messages: changes and breaking changes. This wasn't considered a breaking change, but rather a bugfix. It was difficult to predict that many people used this this behavior. But it should probably go into the migration docs (which need some love)

Sho' 'nuff. Those are the ones. I was looking for changes related to transclusion, but this is clearly a change in isolate scope that is incidental to transclusion. Thanks!

This change broke our code as well. It would be nice to have some easy declarative way of accessing those directive's person variables and using them in the parent scope. Looks like it is a common use case after seeing how many people were using it before this bug was fixed in 1.2.18.

Here is the demo that works in 1.2.17 and broken since 1.2.18

http://plnkr.co/edit/QswOxN?p=preview

@evgenyneu: Assuming you control both the inner content and the container directive, we've found that using ng-include instead of transclude is clear, easy, and gives us the inheritance pattern we want. We simply pass a template name as an attribute on the outer directive declaration, and place an ng-include directive in the same place we formerly had the transclude directive. Problem solved.

If you want the inner content template to be inline on the view template, then just use ng-template to wrap the template in the same position you were using before.

@xmlilley, excellent tip, thank you so much. It is the cleanest approach indeed.

Here is the demo: http://plnkr.co/edit/4MwEL3?p=preview

Guys you know you can just do this instead right http://plnkr.co/edit/fw7thti1u4F9ArxsuYkQ?p=preview --- it's not perfect, but it gets you there

@caitp, nice one, thanks. We have a lot of solutions!

For those watching this issue, I've created an 'improved' ng-transclude directive, the attribute value of which defines the internal scope sought after and can be one of 3:

  • silbing - The transcluded contents scope is a sibling one to the element where transclusion happens. That's the current ng-transclude behaviour.
  • parent - The transcluded contents scope is that of the element where transclusion happens.
  • child - The transcluded contents scope is child scope to the scope of the element where transclusion happens.

Example usage:

template: 
  '<div ng-transclude="parent">' +
  '</div>'

Full example

See this plunk for an example of all.

The output looks like so:

image

Source

.config(function($provide){
    $provide.decorator('ngTranscludeDirective', ['$delegate', function($delegate) {
        // Remove the original directive
        $delegate.shift();
        return $delegate;
    }]);
})

.directive( 'ngTransclude', function() {
  return {
    restrict: 'EAC',
    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 iScopeType = $attrs['ngTransclude'] || 'sibling';

      switch ( iScopeType ) {
        case 'sibling':
          $transclude( function( clone ) {
            $element.empty();
            $element.append( clone );
          });
          break;
        case 'parent':
          $transclude( $scope, function( clone ) {
            $element.empty();
            $element.append( clone );
          });
          break;
        case 'child':
          var iChildScope = $scope.$new();
          $transclude( iChildScope, function( clone ) {
            $element.empty();
            $element.append( clone );
            $element.on( '$destroy', function() {
              iChildScope.$destroy();
            });            
          });
          break;
      }
    }
  }
})

@Izhaki - FWIW, +10. Doesn't break the current conventions, but adds a clean, declarative way of voluntarily accessing a common use-case. Thanks!

:+1: for @Izhaki's solution. I will be using this as is but would feel much more comfortable if it were included in angular.

+1

@Izhaki +1, great example!

Thanks for the custom transclude directive. I ended up just making a separate custom transclude instead of overriding the default. There are a couple function calls in your patch that I'm not sure where they come from. minErr() and startingTag()

@dehru - these functions are internal to AngularJS.

@Izhaki thanks a lot! I have forked your plunk which repeats the transcluded content, if anyone interested.
It's worth mentioning that ng-transclude="parent" might not work the way you would expect.
http://plnkr.co/edit/S6ngqz?p=preview

plunker_ng-transclude_ng-repeat

@Izhaki Very nice.
Looks like something that should be in a pull request to angular..
At least in a separate directive with a Github repo, do you think that you can publish it (so I can offer some changes....)?
Thanks

Izhaki, this is awesome. I _heart_ you.

@Izhaki This is a very nice solution to the problem of child scopes. Thanks.

I am confused though, and was wondering if you could explain, how the inheritance of the $transclude function carries from the outer directive to the ngTransclude directive? It is not explicitly stated anywhere official that I could find, but I assumed that transclude: true had to be used on the directive to use the $transclude function in the link function. After playing with the code a bit, I found using transclude: true actually breaks the code. What's going on here?

I fought with this for days as well as the fact that transclude inserts the transcluded elements within the transclude placeholder instead of replacing it with them.

I have found that ng-transclude-replace handles both issues! http://gogoout.github.io/angular-directives-utils/#/api/ng-directives-utils.transcludeReplace

I don't need to propagate the ng-repeat directive down into the repeated directive itself or anything of the sort! All appears to be working, including all validations/bindings.

Here is a snippet of my code:

<form-field label="Roles" required>
    <checkbox-group>
        <checkbox ng-repeat="role in roles" label="{{role.description}}">
            <input type="checkbox" name="selectedRoles" ng-model="role.selected" value="{{role.description}}" ng-required="!hasRole" />
        </checkbox>
    </checkbox-group>
</form-field>

@abobwhite Thanks for mentioning ng-transclude-replace , great fix.

I've recently run into this problem. While @Izhaki's worked for me, I'm curious if a best practice has emerged in the time since this discussion. In particular, has there been any interest in making ng-transclude="sibling | parent | child" part of angular core?

@telekid - We don't have plans to include this feature in core right now.

I see there are solutions and work arounds, but it would be very convenient if there was a way to access the directive's inside scope.

I would like to point out that I updated the transclude mod that @Izhaki created to angular 1.5 so that it works with multi-slot transclusion.
Branch: https://github.com/NickBolles/ngTranscludeMod/tree/Angular1.5-multi-slot
PR: https://github.com/Izhaki/ngTranscludeMod/pull/2
Plunker: http://plnkr.co/edit/5XGBEX0muH9CSijMfWsH?p=preview

What I ended up doing was just using $parent. It's the closet to vanilla without having to add too many things.

So I have something like:

angular.module('test').directive('myDirectiveWithTransclusion', function() {
     return {
          restrict: 'E'
          transclude: {
               transcludeThis: 'transcludeThis'
          }
          template: "<div ng-repeat='item in array'><div ng-transclude='transcludeThis'></div></div>"
     }
})
<my-directive-with-transclusion>
     <transclude-this>
          {{$parent.item}}
     </transclude-this>
</my-directive-with-transclusion>

Hi @moneytree-doug : I use this solution you provided, but I find the scope of transclude-this is still the directive scope, not the child of new scope which is generated by ng-repeat. Can you give me some suggestions?

@szetin Could you put up a jsfiddle showing me what you expect vs what is happening?

Was this page helpful?
0 / 5 - 0 ratings