1

I'm working with a pre-existing template trying to Angularize it.

I have 3 directives, which is basically a card, a card-header and card-body:

<card>
 <card-header title="My Card">
 <input type="text" ng-model="userSearch" />
 </card-header>
 <card-body>
 <card ng-repeat="item in object | filter:userSearch">
 <card-body>{{ item.name }}</card-body>
 </card>
 </card-body>
</card>

I'm sure you can see the issue... I can't get the filter to pick up the model due to scoping issues. Because I have my own html within the directives, I need to use the transclude: true, and from my understanding that creates its own scope.

Card:

return {
 restrict: 'AE',
 transclude: true,
 replace: true,
 scope: false,
 template: '<div class="card" ng-transclude></div>',
}

Card Header:

return {
 restrict: 'AE',
 requires: 'card',
 transclude: true,
 replace: true,
 scope: false,
 scope: {
 title: '@',
 secondary: '@',
 theme: '@'
 },
 template: '<div class="card-header" ng-class="theme"><h2 ng-if="title">{{ title }}<small>{{ secondary }}</small></h2><div ng-transclude></div></div>',
}

Card Body:

return {
 restrict: 'AE',
 requires: '^card',
 transclude: true,
 replace: true,
 scope: false,
 scope: {
 padding: '@',
 theme: '@'
 },
 template: '<div class="card-body" ng-class="theme" ng-transclude></div>',
 link: function($scope, $element, $attributes) {
 if($scope.padding)
 angular.element($element[0]).addClass('card-padding');
 }
}

Seems like it should be a simple concept, but I've no idea how I can get around this when I have my own scope items, but need to transclude and have my own scope items.

asked May 22, 2015 at 14:25
6
  • 1
    Can you create a plunker? Commented May 22, 2015 at 14:28
  • Could having a controller on the Card work? The child directives have access to the parent directive controller: thinkster.io/egghead/directive-communication Commented May 22, 2015 at 14:30
  • Hmm I could do, but the issue is if I have card directives nested in each other, I imagine that could also cause issues? Shall have a play. Commented May 22, 2015 at 14:41
  • Why do you set scope: false then create isolated scopes? Are you sure what param will be taken into account exactly? Commented May 22, 2015 at 14:56
  • @top.dev Not sure, removing it has no effect - although I do understand what the issue is now, but can't think how to get get around it atm. Commented May 22, 2015 at 15:15

2 Answers 2

1

First, I think maybe you have a markup issue. Here is what I think you meant:

<card>
 <card-header title="My Card">
 <input type="text" ng-model="userSearch" />
 </card-header>
 <card-body>
 <card ng-repeat="item in object | filter:userSearch">
 <card-body>{{ item.name }}</card-body>
 </card>
 </card-body> <!--this was card-header, which doesn't make sense -->
</card>

When you use ng-transclude inside of a directive, the content that is transcluded uses a new scope that is a sibling of the directive scope. So, if you were to analyze your scope tree, here is what you'd have (A is the parent scope of the entire block, () indicates an isolated scope):

<card A>
 <card-header A.B.(C)> 
 <input A.B.D ng-model="A.B.D.userSearch"> 
 </card-header>
 <card-body A.E.(F)>
 <card A.E.G.H ng-repeat="A.E.G.H.item in A.E.G.object | filter: A.E.G.userSearch">
 <card-body A.E.I.(J)>{{A.E.I.K.item.name}}
 </card>
 </card-body>
</card>

Note a few things (besides the obvious "that's a lot of scopes!):

A.B.D.userSearch is an entirely different property than A.E.G.userSearch. A.E.G does not prototypically inherit from A.B.D. This is why the filter doesn't work.

Also note that A.E.G.H.item is also a different property that A.E.I.K.item - this won't work either.

How to fix:

The easiest way to fix is to not use ng-transclude, but use manual transclusion and take control of the scope used by the transcluded content.

For example, the card transclusion would change to:

template: '<div class="card" transclude-target></div>'
link: function(scope, element, attr, ctrl, transclude) {
 transclude(scope, function(clone, scope){
 element.find('[transclude-target]').append(clone);
 }
}

Aside: ng-transclude essentially does:

link: function(scope, element, attr, ctrl, transclude) {
 transclude(scope.$parent.$new(), function(clone, scope){
 element.find('[ng-transclude]').append(clone);
 }
}

What this does is make the transclusion use the directive scope rather than a sibling of the directive scope (or even a new scope)

The scope tree becomes:

<card A>
 <card-header A.(B)> 
 <input A.(B) ng-model="A.(B).userSearch"> 
 </card-header>
 <card-body A.(C)>
 <card A.(C) ng-repeat="A.(C).D.item in A.(C).object | filter: A.(C).userSearch">
 <card-body A.(C).D>{{A.(C).D.item.name}}</card-body>
 </card>
 </card-body>
</card>

Still not quite right (the isolated directives are breaking the inheritance chain we need).

Changing the other two directives (card-header and card-body) to use scope.$parent:

link: function(scope, element, attr, ctrl, transclude) {
 transclude(scope.$parent, function(clone, scope){
 element.find('[transclude-target]').append(clone);
 }
}

Yield's the following scope tree (now your filter will work {{item.name}} should display the correct version)

<card A>
 <card-header A.(B)> 
 <input A ng-model="A.userSearch"> 
 </card-header>
 <card-body A.(C)>
 <card A.E ng-repeat="A.E.item in A.object | filter: A.userSearch">
 <card-body A.E.(D)>{{A.E.item.name}}
 </card>
 </card-body>
</card>

I'm sure I've made a mistake in this somewhere, but I think it should explain what's going on. I wish my explanation were simpler, but it's the best I can do.

answered May 23, 2015 at 5:16
Sign up to request clarification or add additional context in comments.

6 Comments

Woah thanks. I totally get what you're going for and I now have it working... more or less. The element.find part didn't work, but replacing it with angular.element(element[0]).append(clone); does. Obviously this doesn't target the transclude-target bit but I still can't figure out how to target it. So right now it just appends it into the inner-most element.
My fault. angular.element.find() doesn't support selectors. I usually have jQuery installed along with angular. jqLite.find does support tag names, so try changing the markup to <transclude-target></transclude-target> and using find('transclude-target').
Ah nice one :) Another thing, if I try adding a scope to ma-card (<ma-card size="small"></ma-card>), it doesn't load the inner ma-card directive. If it's a whole different issue, don't worry ha.
When you say "add scope", does that mean you set scope:true or use an isolated scope (scope: {})?
Ah, managed to get around it using links attr.size, apply it to the scope and I'm sorted. Thanks for your help.
|
0

This is my understanding of your problem:

If inspecting your last directive, what I see is no model defined in the template.

Now when you specify template, it replaces the html inside the directive element, so that being the reason of your model not appearing.

That being said, you will have to include model inside your template if item.name is in card-body directive.

Now when you use ng-transclude, it puts the original html back, that is why you are able to somewhat solve the problem(but scope causing the issue).

You will have to change the template as follows and will have to include item also in the isolated scope definition.

template: '<div class="card-body" ng-class="theme">{{item.name}}</div>'

If you don't want to modify it and want to use ng-transclude, there are two ways to call parent scope variables from child scope,

  1. Use properties of a scope object instead.

So instead of having itmes as $scope.items you can instead use an object and have items as property: $scope.itemModel.items

Now if you modify these, they will reflect in parent scope as well. And its faster due to nature of javascript.

  1. Not recommended but you can always call parent scope variables using $parent both in view and controller.

Check if these works for you.

answered May 23, 2015 at 6:31

Comments

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.