Template Transclusion in AngularJS

TL;DR

Standard ng-transclude can’t pass parent data to transcluded content. Build a custom cr-transclude directive that extends the transclude scope with context data.

The Problem

You’re building a reusable list component. You want users to define how each item renders:

<my-list items="$ctrl.movies">
<div>{{ name }} - {{ year }}</div>
</my-list>

The transcluded template needs access to each item’s data. But ng-transclude doesn’t support this.

Here’s what happens: transcluded content gets the grandparent’s scope, not the parent that renders it. Your template can access $ctrl.movies but not the individual item being rendered.

Why This Happens

AngularJS transclusion creates a scope that inherits from where the template was defined, not where it’s rendered.

When my-list iterates and renders each item, the transcluded content has no way to receive the current item data.

Angular 2+ solves this with ngTemplateOutlet and context objects. AngularJS needs a workaround.

The Solution

Create a directive that watches a context binding and extends the transclude scope:

angular.module('app').directive('crTransclude', function() {
return {
restrict: 'EA',
transclude: true,
link: function($scope, $element, $attrs, ctrl, $transclude) {
var childScope;
var context;
$transclude(function(clone, transcludedScope) {
childScope = transcludedScope;
$element.append(clone);
updateScope(childScope, context);
});
$scope.$watch($attrs.context, function(newVal) {
context = newVal;
updateScope(childScope, context);
});
function updateScope(scope, varsHash) {
if (!scope || !varsHash) return;
angular.extend(scope, varsHash);
}
}
};
});

The key insight: we’re doing exactly what ng-repeat does internally. It creates child scopes and attaches properties like $index directly to them.

Using It

Your list component template becomes:

<ul>
<li ng-repeat="item in $ctrl.items">
<cr-transclude context="item"></cr-transclude>
</li>
</ul>

Now the consumer can write:

<my-list items="$ctrl.movies">
<div>{{ name }} ({{ year }})</div>
</my-list>

Each name and year comes from the current item. The grandparent scope ($ctrl) is still accessible for headers or other data.

Limitations

The transcluded content must be a direct child of the component. You can’t nest it deeper without additional work.

Context updates are one-way. Changes in the transcluded template won’t propagate back to the source object unless you bind to properties on it.

References