Force scope to named slot with ng-transclude
Asked Answered
C

1

1

I've a directive as follows:

<selectable-item-list items="model.items">
    <item-template>
          <span ng-bind="item.text"></span>
    </item-template>
</selectable-item-list>

The issue is in the <item-template>, where item would be a reference of currently iterated item when an internal ng-repeat is bound inside <selectable-item-list>.

AFAIK, it seems like transclusions can't see directive's scope, thus, that item.text can't be bound because there's no item at all.

How would you solve this scenario? Previously I was manually-transcluding <item-template> but that other approach had other downsides/issues.

Here's a runnable code snippet that works as sample of my real-world case:

var app = angular.module("app", []);

app.controller("some", function() {
  this.items = [{
    text: "hello"
  }, {
    text: "bye"
  }];
});

app.directive("test", function() {
  return {
    template: `<ol>
                  <li ng-repeat="item in items">
                      <div ng-transclude="itemTemplate"></div>
                  </li>
                </ol>`,
    transclude: {
      "itemTemplate": "itemTemplate"
    },
    scope: {
      "items": "="
    }
  }
});
<script src="https://code.angularjs.org/1.5.7/angular.js"></script>

<div ng-app="app" ng-controller="some as some">
  <test items="some.items">
    <item-template>
      <span ng-bind="item.text"></span>
    </item-template>
  </test>
</div>
Cas answered 8/7, 2016 at 12:56 Comment(6)
Can you show us a simplified version of the selectable-item-list and item-template template's. That might make it easier to wrap my head around it.Starnes
Also, you're right about "AFAIK, it seems like transclusions can't see directive's scope". This is so that the directive aren't tied together, defeating the purpose of transcluded directives.Starnes
@MattLishman Thanks for your comments. I'll try to put here a sample code so we can investigate further together. I'll do it in some hours.......Godbeare
@MattLishman Finally I did in 5 minutes. Check my updated question, I've added a runnable code snippet ;)Godbeare
@MattLishman In the whole snippet you'll see that both items are rendered but with no content, because obviously item can't be accessed from the item template....Godbeare
@MattLishman I could find the definitive solution myself! See my added answer ;)Godbeare
C
1

I had a wrong assumption! When I said that a transcluded content couldn't access the containing directive scope I was wrong because of this other Q&A: Why ng-transclude's scope is not a child of its directive's scope - if the directive has an isolated scope? which is absolutely outdated.

In fact, there's another answer as part of the same Q&A where someone has described that now this has been fixed and a transcluded content can access its direct directive scope using $parent.

So I fixed my issue just replacing the item property access with $parent.item and it worked!

I've added a working code snippet which has this fix:

var app = angular.module("app", []);

app.controller("some", function() {
  this.items = [{
    text: "hello"
  }, {
    text: "bye"
  }];
});

app.directive("test", function() {
  return {
    template: `<ol>
                  <li ng-repeat="item in items">
                      <div ng-transclude="itemTemplate"></div>
                  </li>
                </ol>`,
    transclude: {
      "itemTemplate": "itemTemplate"
    },
    scope: {
      "items": "="
    }
  }
});
<script src="https://code.angularjs.org/1.5.7/angular.js"></script>

<div ng-app="app" ng-controller="some as some">
  <test items="some.items">
    <item-template>
      <span ng-bind="$parent.item.text"></span>
    </item-template>
  </test>
</div>
Cas answered 8/7, 2016 at 19:22 Comment(7)
Glad you found a working solution. I was trying to find the SO posts you linked before but couldn't. I would be careful though, from that post the top answer makes a good point "ng-transclude designed to allow directives to work with arbitrary content, and isolated scopes are designed to allow directives to encapsulate their data." I would be careful that you have now tied the implementation of <item-template> to <test> i.e test always needs to have a $parent.item.text available. If you change how test works, you could break item-templates too. Good answer though.Starnes
@MattLishman Yeah, I'm aware about that this couples the template with the whole directive, but, after all, it's an item template of an item designed for a specific directive. Coupling is fine here, isn't it?Godbeare
Agreed, I think if it's clear that they are coupled, there is no issue with it. Just added that to ensure others think about it. It's worth noting that I think all scopes have a $parent property, so this technique can be used elsewhere.Starnes
@MattLishman Yup, you're right. BTW thanks to this approach I can remove previous over-engineered solution that consisted in compiling the item template manually with $compile and this had a lot of issues..Godbeare
@MattLishman IMHO I believe that this is a design flaw in Angular 1.x, because it would be nice to have something like this: "<div ng-transclude="itemTemplate" scope="item"></div>" so you can define which would be the scope within the whole transcluded content, and we wouldn't need item.text or $parent.item.text, but just textGodbeare
Yes, I agree, that would be nice. Putting it that way feels like it might be possible to do in angular 1 in a better way. I will continue to think about it. It's 11:45 PM here though, too late to be thinking about this now... haStarnes
@MattLishman Lol, here is 0:46PM which is even worse than you to think about this ;DGodbeare

© 2022 - 2024 — McMap. All rights reserved.