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

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

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
unscene commented Apr 11, 2013

+1

@MarcoPil

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

@nbauernfeind

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

@cristatus

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
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
paynen commented Jun 19, 2013

+1

@cristatus

Looks like it's fixed in master 45f9f62

@cristatus cristatus referenced this issue Jul 27, 2013
@IgorMinar IgorMinar fix($compile): always instantiate controllers in parent->child order
Previously it was possible to get into a situation where child controller
was being instantiated before parent which resulted in an error.

Closes #2738
45f9f62
@petebacondarwin
Member

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

@gustavohenke

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

@nbauernfeind

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

@rrimer
rrimer commented Feb 5, 2014

+1

@cztomsik

it's still there.

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

@YourDeveloperFriend

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 the type: bug label Mar 6, 2014
@jeffbcross jeffbcross added this to the 1.3.x milestone Mar 6, 2014
@ivan-saorin

+1

@McNull
McNull commented Mar 17, 2014

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

@bvaughn
bvaughn commented May 2, 2014

+1

@petebacondarwin petebacondarwin self-assigned this May 7, 2014
@petebacondarwin
Member

I have a fix for this at #7499

@petebacondarwin petebacondarwin added a commit to petebacondarwin/angular.js that referenced this issue May 17, 2014
@petebacondarwin petebacondarwin fix($compile): fix nested isolated transclude directives
Closes #1809
Closes #7499
0c936bb
@petebacondarwin petebacondarwin modified the milestone: 1.3.0-beta.10, 1.3.0 May 19, 2014
@petebacondarwin petebacondarwin added a commit to petebacondarwin/angular.js that referenced this issue May 19, 2014
@petebacondarwin petebacondarwin fix($compile): fix nested isolated transclude directives
Closes #1809
Closes #7499
cefee8f
@petebacondarwin petebacondarwin added a commit to petebacondarwin/angular.js that referenced this issue May 22, 2014
@petebacondarwin petebacondarwin fix($compile): fix nested isolated transclude directives
Closes #1809
Closes #7499
2d4aac7
@petebacondarwin petebacondarwin added a commit to petebacondarwin/angular.js that referenced this issue May 22, 2014
@petebacondarwin petebacondarwin fix($compile): fix nested isolated transclude directives
Closes #1809
Closes #7499
a466e57
@endorama

+1

@btford btford modified the milestone: 1.3.0-beta.11, 1.3.0-beta.10 May 23, 2014
@vojtajina vojtajina added a commit to vojtajina/angular.js that referenced this issue May 28, 2014
@petebacondarwin @vojtajina petebacondarwin + vojtajina fix($compile): fix nested isolated transclude directives
Closes #1809
Closes #7499
3d32920
@vojtajina vojtajina added a commit to vojtajina/angular.js that referenced this issue May 28, 2014
@petebacondarwin @vojtajina petebacondarwin + vojtajina fix($compile): fix nested isolated transclude directives
Closes #1809
Closes #7499
c39e441
@vojtajina vojtajina added a commit to vojtajina/angular.js that referenced this issue May 29, 2014
@petebacondarwin @vojtajina petebacondarwin + vojtajina fix($compile): fix nested isolated transclude directives
Closes #1809
Closes #7499
a229720
@caitp caitp added a commit to caitp/angular.js that referenced this issue Jun 13, 2014
@petebacondarwin @caitp petebacondarwin + caitp fix($compile): fix nested isolated transclude directives
Closes #1809
Closes #7499
a7def05
@revolunet revolunet added a commit to revolunet/angular.js that referenced this issue Jun 16, 2014
@petebacondarwin @revolunet petebacondarwin + revolunet fix($compile): fix nested isolated transclude directives
Closes #1809
Closes #7499
3e46974
@gabrielmaldi
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
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
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
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
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

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

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

@funkjunky

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

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

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) {
            //??
        },
    }
}
@wtfribley

@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

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

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

@gkalpak
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 join this conversation on GitHub. Already have an account? Sign in to comment