Skip to content
This repository has been archived by the owner on Apr 12, 2024. It is now read-only.

angular 1.2.18: ng-repeat problem with transclude #7874

Closed
sergicontre opened this issue Jun 17, 2014 · 48 comments
Closed

angular 1.2.18: ng-repeat problem with transclude #7874

sergicontre opened this issue Jun 17, 2014 · 48 comments

Comments

@sergicontre
Copy link

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

@caitp
Copy link
Contributor

caitp commented Jun 17, 2014

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

@Narretz
Copy link
Contributor

Narretz commented Jun 17, 2014

(maybe) related: #7842

@petebacondarwin
Copy link
Member

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

@Izhaki
Copy link
Contributor

Izhaki commented Jul 1, 2014

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.

@petebacondarwin
Copy link
Member

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.

@petebacondarwin
Copy link
Member

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

@petebacondarwin
Copy link
Member

@Izhaki
Copy link
Contributor

Izhaki commented Jul 1, 2014

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

@Izhaki
Copy link
Contributor

Izhaki commented Jul 1, 2014

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

@petebacondarwin
Copy link
Member

:-) 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?

@Izhaki
Copy link
Contributor

Izhaki commented Jul 1, 2014

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

@luboid
Copy link

luboid commented Jul 1, 2014

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

@caitp
Copy link
Contributor

caitp commented Jul 1, 2014

@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)

@caitp
Copy link
Contributor

caitp commented Jul 1, 2014

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

@Izhaki
Copy link
Contributor

Izhaki commented Jul 1, 2014

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

@petebacondarwin
Copy link
Member

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

@luboid
Copy link

luboid commented Jul 1, 2014

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

@kencaron
Copy link

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

@xml
Copy link

xml commented Aug 4, 2014

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?

@caitp
Copy link
Contributor

caitp commented Aug 4, 2014

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

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

@Narretz
Copy link
Contributor

Narretz commented Aug 4, 2014

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)

@xml
Copy link

xml commented Aug 4, 2014

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!

nidico added a commit to liqd/adhocracy3 that referenced this issue Aug 15, 2014
As Angular-1.2.18 fixed a bug with transclude scoping, we need to use a
custom transclude directive (which we call inject).

See upstream issue and especially the following:

angular/angular.js#7874 (comment)
@evgenyneu
Copy link

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
Copy link

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

@Izhaki
Copy link
Contributor

Izhaki commented Aug 26, 2014

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

@xml
Copy link

xml commented Aug 27, 2014

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

@ziadsawalha
Copy link

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

@heycalmdown
Copy link

+1

@sebholstein
Copy link
Contributor

@Izhaki +1, great example!

@dehru
Copy link

dehru commented Sep 19, 2014

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()

@petebacondarwin
Copy link
Member

@dehru - these functions are internal to AngularJS.

@thevictor13
Copy link

@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

@Urigo
Copy link
Contributor

Urigo commented Sep 21, 2014

@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
Copy link
Contributor

Izhaki commented Sep 22, 2014

@elbow-jason
Copy link

Izhaki, this is awesome. I heart you.

@evanyeung
Copy link

@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?

@abobwhite
Copy link

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>

@aparij
Copy link

aparij commented May 28, 2015

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

vikasrohit pushed a commit to appirio-tech/topcoder-app that referenced this issue Oct 17, 2015
-- Created new directive for responsive carousel
-- Created new directive for fixing scope issue(angular/angular.js#7874 (comment)) with ng-transclude
@telekid
Copy link

telekid commented Apr 26, 2016

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?

@petebacondarwin
Copy link
Member

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

@moneytree-doug
Copy link

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.

@NickBolles
Copy link

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: Izhaki/ngTranscludeMod#2
Plunker: http://plnkr.co/edit/5XGBEX0muH9CSijMfWsH?p=preview

@moneytree-doug
Copy link

moneytree-doug commented May 4, 2016

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>

@sy-tian
Copy link

sy-tian commented Jan 5, 2017

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?

@moneytree-doug
Copy link

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

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests