Question

My goal is to create a flexible set of directives for reusable and lightweight UI elements. Each has an isolated scope, and many of them transclude content. I want each directive to be a black box - the user ideally wouldn't need to know whether it nests another directive internally when writing the content to be transcluded.

According to the Angular guide to directives:

The transclude option changes the way scopes are nested. It makes it so that the contents of a transcluded directive have whatever scope is outside the directive, rather than whatever scope is on the inside. In doing so, it gives the contents access to the outside scope.

I have found that this works as described when using a single directive. However, if there is another directive nested in that one that also transcludes the content, then the transcluded content is resolved within the scope of the outer directive, rather than the scope that is on the outside. This is a problem, because it prevents users from knowing in what scope their transcluded content will be resolved!

For example: (fiddle)

.controller('main', function ($scope) {
    $scope.value = '"main"';
    $scope.expected = $scope.value;
})

.directive('outer', function () {
    return {
        restrict: 'E',
        replace: true,
        transclude: true,
        scope: { expected:'=' },
        controller: function ($scope) {
            $scope.value = '"outer"';
        },
        template: '<div><inner expected="expected"><span ng-transclude></span></inner></div>'
    };
})

.directive('inner', function () {
    return {
        restrict: 'E',
        replace: true,
        transclude: true,
        scope: { expected:'=' },
        controller: function ($scope) {
            $scope.value = '"inner"';
        },
        template: '<div><span>\'value\' is expected to be resolved in scope {{expected}}, and is resolved in scope </span><span ng-transclude></span></div>'
    };
})

And the HTML:

<div ng-controller="main">
    <inner expected="value">
        <span>{{value}}</span>
    </inner>
    <hr/>
    <outer expected="value">
        <span>{{value}}</span>
    </outer>
</div>

Inside the <inner></inner> element {{value}} is evaluated within the parent scope as "main" (as expected). However, inside the <outer></outer> element {{value}} is evaluated within the isolated scope of outer as "outer" (not expected). In this way, the template of the directive can affect the scope in which the transcluded content is resolved!

Is there any way to work around this issue?

Was it helpful?

Solution 2

After some tinkering I found a solution. I created two new directives to replace the default ng-transcude, and a helper service that stores the state shared between the directives. Together they allow the deep transclusion that I was looking for.

The first stores $transclude into the service; it must be used within every directive that can be given transcludible content in a template:

controller: ['$transclude', 'TransclusionHelper', function($transclude,TransclusionHelper) {
    TransclusionHelper.setTransclusionFn($transclude);
}],

The second performs the transclusion of the content to its final location; it need not be used in directives that only pass through their transcluded content to a child:

link: ['$element','TransclusionHelper', function($element,TransclusionHelper) {
    TransclusionHelper.transclude(function(clone) {
        $element.empty();
        $element.append(clone);
    });
}],

Some finesse is required in the service to manage state; if several calls to setTransclusionFn are made before a call to transclude, only the first should be stored and used when transclude is called.

OTHER TIPS

This really sucks indeed! Angular only calls one parent in isolated scope, and if it doesn't find what it needs it stops looking. To solve this, you can manually call the scope's parent like so:

controller: function ($scope) {
    $scope.value = $scope.$parent.value || '"outer"';
}

This will "make" him look further up the chain.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top