2

I am building a directive that can be use in different controllers, and I would like to be able of bind the directive to a particular property of my $scope.

I would like to do something like this:

<div ng-app="myapp">
    <div ng-controller="myController">
        <my-directive-wrapper ng-model="mymodel">
            <my-directive-inner ng-repeat="item in items" />
        </my-directive-wrapper>
    </div>
</div>

With this model:

$scope.mymodel = { 
    name : "Transclude test",
    items : [
        { title : "test1" },
        { title : "test2" },
        { title : "test3" }
    ]
};

So the directive myDirectiveWrapper gets $scope.mymodel as scope, and nothing else. Then I may put the directive twice, pointing to a different property each.

I have a demo project with the problem here: http://jsfiddle.net/vtortola/P8JMm/3/

And the same demo working normally (without limiting the scope) here: http://jsfiddle.net/vtortola/P8JMm

The question is, how to indicate in the use of my directive that I want to use a particular property of my $scope as scope of my directive. It should be possible to bind the directive to arbitrary properties in the same $scope.

Cheers.

vtortola
  • 34,709
  • 29
  • 161
  • 263
  • Have you looked at the `$transclude` function which is passed onto the parameter of the link function? It allows you to pass an object as the scope for the transcluded part and a function for DOM substitution. Checkout this SO thread http://stackoverflow.com/questions/13183005/what-exactly-do-you-do-with-the-transclude-function-and-the-clone-linking-functi – javaCity Jul 10 '14 at 12:50
  • 1
    how abt this http://jsfiddle.net/P8JMm/4/ – Nidhish Krishnan Jul 10 '14 at 12:58
  • @NidhishKrishnan The variable name is not being bound, it should say on the screen "Transclude test". The directive is using the $scope, rather than only part of it. Good idea though. – vtortola Jul 10 '14 at 13:10
  • @javaCity `my-directive-wrapper` is the one that should get that partial model, not the transcluded part. – vtortola Jul 10 '14 at 13:14
  • @vtortola: The content of the wrapper directive is always going to be `` or could it be something else ? – gkalpak Jul 10 '14 at 13:15
  • It could be something else. The wrapper is just that, a wrapper that my wrap any element or elements. – vtortola Jul 10 '14 at 13:22
  • And what would be the benefit of using this directive ? – gkalpak Jul 10 '14 at 13:43

2 Answers2

2

So the basic answer to this question is - you can do what you want to do, but it is a bit more complicated than you might think. To understand what is happening here you need to know about scopes in angular. A scope is essentially an object that contains the data accessible to the view. There are (at least) three ways scopes operate in angular:

  • Isolated - In this case angular basically creates a brand new scope for the directive. None of the properties are copied over.
  • Extended - In this case you would start with the root scope but make a shallow copy of it. Objects that are changed will be changed in the root scope but primitives will not be.
  • Shared - In this case you share share some or even all of the data with the root scope.

Based on your question above, what you what to do here is to extend the parent scope - copying an object to a property with a specific name in the newly created child scope. The way to get this behavior is to manually create a new child scope before the transclude. The two key lines of code to do this are:

// create a "new" scope
var childScope = scope.$new();

// extend using the model binding provided
angular.extend(childScope, scope[iAttr.myModel]);

In the context of your directive this looks like:

.directive('myDirectiveWrapper', ['$compile', function ($compile) {
   return {
        transclude: true,
        restrict: 'E',
        compile: function (element, attrs, transclude) {
            var contents = element.contents().remove();
            var compiledContents;
            return function(scope, iElement, iAttr) {
               var childScope = scope.$new();
               angular.extend(childScope, scope[iAttr.myModel]);
                if (!compiledContents) {
                    compiledContents = $compile(contents, transclude);
                }
                compiledContents(childScope, function(clone, childScope) {
                         iElement.append(clone); 
                });
            };
        },
        template: "<div><h3>{{ name }}</h6><a class='back'>Back</a><div ng-transclude class='list'></div><a class='next'>Next</a>"
    }
}])

Now you can specify any variable that you want as the "model" for the child scope and you can then access that directly in the contents of your transcluded code!

SEE THE FIDDLE: http://jsfiddle.net/P8JMm/7/

EDIT: Just for fun, I created a more complicated use case for this directive: http://jsfiddle.net/P8JMm/9/

Note - angular site also has some really good resources to understand scope better. See here.

drew_w
  • 10,320
  • 4
  • 28
  • 49
  • 1
    When you cann `angular.extend` it will make a copy of all the primitives on mymodel so this will break two way binding. – rob Jul 10 '14 at 14:00
  • @rob You are correct. For that the OP would need to assign a specific variable or would need to use objects for two way binding rather than primitives. Unfortunately, there are limitations no matter what the approach... – drew_w Jul 10 '14 at 14:06
1

If you want two way binding to work it's going to be a lot easier to just create a variable on your directive scope rather than apply mymodel directly onto the directive scope.

HTML

<div ng-app="myapp">
    <div ng-controller="myController">
        <my-directive-wrapper model="mymodel">
            <my-directive-inner ng-repeat="item in mymodel.items" />
        </my-directive-wrapper>
    </div>
</div>

Directive

.directive("myDirectiveWrapper", function(){
    return {
        scope: {
            model: '='        
        },
        restrict: 'E',
        transclude: true,
        link: function(scope, element, attrs, controller) {

        },
        template: "<div><h3>{{ model.name }}</h6><a class='back'>Back</a><div ng-transclude class='list'></div><a class='next'>Next</a>"
    }
})

http://jsfiddle.net/kQ4TV/

If you don't care about two way binding I suppose you could do something like this but I wouldn't recommend it:

.directive("myDirectiveWrapper", function(){
    return {
        scope: {
            model: '='        
        },
        restrict: 'E',
        transclude: true,
        link: function(scope, element, attrs, controller) {
            angular.extend(scope, scope.model);
        },
        template: "<div><h3>{{ name }}</h6><a class='back'>Back</a><div ng-transclude class='list'></div><a class='next'>Next</a>"
    }
})

http://jsfiddle.net/vWftR/

Here is an example of when that second approach can cause problems. Notice that when you enter something into the input field it will change the directive's name but not the name in the outer scope: http://jsfiddle.net/r5JeJ/

rob
  • 17,995
  • 12
  • 69
  • 94
  • Your solution is also good, but @drew_w's solution is closer to what I wanted despite of the binding problem. – vtortola Jul 10 '14 at 17:12