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

An isolated-scoped-directive's ng-transclude loses parent scope when inside of an ng-repeat. #1809

Closed
nbauernfeind opened this issue Jan 14, 2013 · 35 comments

Comments

@nbauernfeind
Copy link

The fiddle: http://jsfiddle.net/nCfBA/

To reproduce in short: 1) transcluded element needs to reference something in its parent scope, 2) directive's template needs to transclude inside of an ng-repeat. The transcluded element cannot read its original parent scope.

If included outside of the ng-repeat it can read the value in the parent scope (as one would expect).

I posted this to a google group (https://groups.google.com/forum/#!topic/angular/az8_uNV7KyE) but was directed to post it as a possible bug.

I don't mind trying to tackle and fix this, but I really have no idea where to start looking.

@nbauernfeind
Copy link
Author

This issue still exists in 1.0.5. Updated fiddle: http://jsfiddle.net/nCfBA/1/

I'd be happy to fix this, but don't know where to begin.

@unscene
Copy link

unscene commented Apr 11, 2013

+1

@MarcoPil
Copy link

+1
I have spend two days to find out that the problem was a combination of transclusion, isolated scope and ng-repeat :-(

@nbauernfeind
Copy link
Author

I'm still happy to try and fix this =).

@cristatus
Copy link

I am facing exactly the same issue. I think the problem is the $transclude function that keeps the reference of outer scope. I have created a new directive (replacement for ng-transclude) that uses correct scope.

angular.module("my").directive('myTransclude', function() {
    return {
        compile: function(tElement, tAttrs, transclude) {
            return function(scope, iElement, iAttrs) {
                transclude(scope.$new(), function(clone) {
                    iElement.append(clone);
                });
            };
        }
    };
});

However, I am not sure whether this is the correct solution but at least it's working fine for our use cases.

@oravecz
Copy link

oravecz commented Jun 5, 2013

Wow, thanks cristatus. That fixed my problem also where I had two directives on an element and one of those directives (vertical-center) had a transclude that was screwing up the other element's access to a parent scope. The directive with the transclude is shown here. Something pretty simple that enables dynamic blocks to be vertically centered within other blocks. Switching ng-transclude to my-transclude did the trick.

directive( 'verticalCenter', function () {
    return {
        restrict : 'AC',
        template : ' \
            <div style="display: table; width: 100%; height: 100%"> \
                <div style="display: table-cell; vertical-align:middle;" my-transclude> \
                </div>\
            </div>',
        transclude : true,
        scope: false
    }
});

@paynen
Copy link

paynen commented Jun 19, 2013

+1

@cristatus
Copy link

Looks like it's fixed in master 45f9f62

cristatus referenced this issue Jul 27, 2013
Previously it was possible to get into a situation where child controller
was being instantiated before parent which resulted in an error.

Closes #2738
@petebacondarwin
Copy link
Member

Not sure if it is: http://jsfiddle.net/33jQz/

@gustavohenke
Copy link

@petebacondarwin, I'm also a "victim" of this. Do you know of any quick fix for this?

@nbauernfeind
Copy link
Author

@gustavohenke Did you try the quick fix from @cristatus?

@rrimer
Copy link

rrimer commented Feb 5, 2014

+1

@cztomsik
Copy link

it's still there.

ng-transclude with ng-if inside of directive with transclude: true & isolate scope.

@YourDeveloperFriend
Copy link

I was able to recreate this with ng-if:

http://jsfiddle.net/WqD7E/2/

and I can confirm that this line (https://github.com/angular/angular.js/blob/master/src/ng/directive/ngIf.js#L93) within ng-if is calling scope.$new() on a child scope of my directive, and not of the parent scope as you would expect. I bet that's what's happening within ngRepeat as well.

Here is how I overcame this. It sucks, but it works:

.directive('alteredTransclude', function() {
  return {
    compile: function(tElem, tAttrs, transclude) {
      return function(scope, elem, attrs) {
        var newScope = scope.$parent.$parent.$new(); // Call $parent to get to the scope you want
        transclude(newScope, function(clone) {
          elem.append(clone);
        });
      };
    }
  };
})

@jeffbcross jeffbcross added this to the 1.3.x milestone Mar 6, 2014
@ivan-saorin
Copy link

+1

@McNull
Copy link

McNull commented Mar 17, 2014

Plunked a workaround for this, description can be found on stackoverflow.

@bvaughn
Copy link

bvaughn commented May 2, 2014

+1

@petebacondarwin
Copy link
Member

I have a fix for this at #7499

petebacondarwin added a commit to petebacondarwin/angular.js that referenced this issue May 17, 2014
@btford btford removed this from the 1.3.0-beta.10 milestone May 23, 2014
vojtajina pushed a commit to vojtajina/angular.js that referenced this issue May 28, 2014
vojtajina pushed a commit to vojtajina/angular.js that referenced this issue May 28, 2014
vojtajina pushed a commit to vojtajina/angular.js that referenced this issue May 29, 2014
caitp pushed a commit to caitp/angular.js that referenced this issue Jun 13, 2014
@gabrielmaldi
Copy link
Contributor

@petebacondarwin how would you go about making the repeated item also accesible inside the transcluded scope?

This would allow writing custom "repeat" directives effortlessly (delegating to ng-repeat).

http://jsfiddle.net/xU5Cf

Thanks!

@petebacondarwin
Copy link
Member

@gabrielmaldi - you cannot do this with ng-transclude. You could hack it by calling the transclude function yourself and mapping in local properties onto the new scope...

link: function(scope, element, attr, ctrl, trans) {
  trans(function cloneAttachFn(scope, transElement) {
    element.append(transElement);
    scope.xtraProp = ...;
  });
}

@gabrielmaldi
Copy link
Contributor

@petebacondarwin thanks, I followed your advice and assembled this: http://jsfiddle.net/gabrielmaldi/w2YNf

But there's a problem: if you replace the items array with a new one while keeping the same ids, ngRepeat sees no change and doesn't recreate the repeated elements, so the view is not updated because we shadowed the item property. You can repro this by clicking on Update items.

So I came up with this solution: http://jsfiddle.net/gabrielmaldi/FHy4z

Here I'm messing with scopes, creating a new "transcluded" (doesn't get external HTML, just scope inheritance) one and attaching it to the parent of the element with the ngRepeat directive. Then, repeated elements are transcluded (true transclusion, including HTML). This provides access to the transcluded scope (inherited from the first "transclusion") as well as the item ngRepeat attaches (also no shadowing of the item property, so Update items works fine).

I'd really appreciate to hear what you have to say about this approach: is it too hackish or farfetched? can this cause any leaks when destroying scopes (introducing the new "transcluded" scope like this)?

Thanks again for your help!

@petebacondarwin
Copy link
Member

Actually I think that transclude probably should play not part in this kind of directive.
Perhaps you could try this instead: #7874 (comment) or this #7842 (comment)

@gabrielmaldi
Copy link
Contributor

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.

That makes sense and I acknowledge that I'm abusing transclusion a little. I like the approach of just injecting the template HTML inside ngRepeat, went through that road before, but it falls short if the directive introduces an isolate scope.

@funkjunky
Copy link

@petebacondarwin You're solutions are quite elegant, but they don't cover every case. ie. my case, where I use a template URL for my template. The compile function doesn't have access to either the template, or the URL for the template. If it did I could just attach tElement to my element inside ng-repeat and be good to go.

Any ideas on how to solve this?

So for example, my directive has:

{
    templateUrl: 'mytemplate.html',
    transclude: true,
}

with directive template like (but a lot more html warranting the separate template file):

<ng-repeat="item in items">
    <div class="transcluder" ng-transclude></ng-transclude>
</ng-repeat>

and html:

<my-list items="items">
    <strong>{{name}}</strong>
    <p>{{summary}}</p>
</my-list>

I understand using compile etc. It's actually a quite clever solution, however I can't appear to use that solution, because my template is in a separate file and can't be simply written in the compile function.

@IgorMinar
Copy link
Contributor

Your compile fn is called after we fetch the template. So you do have access to the template from compile fn.

@funkjunky
Copy link

where? How would I access that template? This is very intriguing! :D

@petebacondarwin
Copy link
Member

It should already have been added to the directive element
On 3 Jul 2014 06:56, "Jason McCarrell" notifications@github.com wrote:

where? How would I access that template? This is very intriguing! :D


Reply to this email directly or view it on GitHub
#1809 (comment).

@funkjunky
Copy link

so... first, second, third argument to compile? perhaps it is found in one of the injected modules?
The compile prototype looks like:

module.directive('mydirective', function() {
    return {
        compile: function(telement, tattrs, ttransclude) {
            //??
        },
    }
}

@petebacondarwin
Copy link
Member

@wtfribley
Copy link

@petebacondarwin @gabrielmaldi Any word on the proper way to make this method (compile function injecting HTML into an ng-repeat or other transcluding directive) work inside a directive with isolate scope?

@wtfribley
Copy link

@petebacondarwin @gabrielmaldi

By means of example, here's what I'm doing currently:

app.directive('transcludeIntoRepeat', function($compile) {
return {
  restrict: 'E',
  template: '<div></div>',
  replace: true,
  transclude: true,
  scope: {items: '='},
  link: function(scope, element, attrs, ctrl, transclude) {
    transclude(scope, function(clone) {
      var template = angular.element('<div ng-repeat="item in items"></div>');

      template.append(clone);
      element.append($compile(template)(scope));
    });
  }
});

To add a level of complexity, yes I'm using a template and yes replace: true - and it's all good until I try to include a transcluding directive like ng-if in the original html (i.e. clone) which is transcluded into the ng-repeat.

Here's a plunker: http://plnkr.co/edit/aBHOFnpy5U4Eh3R5yjhD?p=preview

I can't help but think I'm going about this the wrong way...

@gabrielmaldi
Copy link
Contributor

@wtfribley I'm currently using this approach: http://jsfiddle.net/gabrielmaldi/FHy4z

I explained it a bit here:

Here I'm messing with scopes, creating a new "transcluded" (doesn't get external HTML, just scope inheritance) one and attaching it to the parent of the element with the ngRepeat directive. Then, repeated elements are transcluded (true transclusion, including HTML). This provides access to the transcluded scope (inherited from the first "transclusion") as well as the item ngRepeat attaches (also no shadowing of the item property, so Update items works fine).

@durga-telsiz
Copy link

Any hints on how to achieve this(access scope) with multi slot transclusion of angularJS 1.5?

@gkalpak
Copy link
Member

gkalpak commented Nov 22, 2016

@durga-telsiz, you are unlikely to get any help on this thread. This is issue is very old (v1.0.x) and closed.

For general support/usage question, you can use one of the support channels. If you think you have found a bug in Angular or want to request a feature, please submit a new issue (if one does not already exist).

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.