Alias
Alias

Reputation: 3081

Angular directives &scope issues

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.

Upvotes: 1

Views: 101

Answers (2)

Joe Enzminger
Joe Enzminger

Reputation: 11190

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.

Upvotes: 1

Kop4lyf
Kop4lyf

Reputation: 4590

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.

Upvotes: 0

Related Questions