How to do a semi template transclusion in AngularJs, using a customize transclude directive.
TL;DR
Custom AngularJs/1 transclude directive that allow the transcluded content access the grandparent scope as before and allow the parent to pass data to it as ng-repeat allows.
The custom directive is available here in GitHub and NPM.
App Component:
<div>{{ $ctrl.grandParentHeader }}</div>
<my-list items="$ctrl.movies">
<div>App data: {{ $ctrl.grandParentHeader }}</div>
<div>Name:{{ name }} Year: {{ year }}</div>
</my-list>
MyList Component:
<ul>
<li ng-repeat="item in $ctrl.items track by item.id">
<cr-transclude context="item"></cr-transclude>
</li>
</ul>
Scenario
When drawing a table on the page, the basic to do so is using ng-repeat.
Now, when wanting incorporate a custom logic and presentation to the table and create a custom table component that does the ng-repeat inside but get the row to paint transcluded from outside, its not possible using the regular ng-transclude directive.
ng-transclude allow accessing the data from the grandparent, not the parent that render the transcluded content. The parent has no options to transfer data to the transcluded child. Meaning if we wanted to do something like this:
grandparent.js
<my-custom-table>
<trn-row>
<td><hero-image id="row.id"></td>
</trn-row>
</my-custom-table>
parent— my-custom-table.compoent.js
<div class="table">
<ng-transclude ng-transclude-slot="trnRow"
ng-repeat="row in $ctrl.rows>
</ng-transclude>
</div>
We can’t.
The trn-row has no access to row from the ng-repeat of the child component.
Other examples could be requirement to create a custom dropdown, carousel, and any other repeater component or even one projection component but with the need of the parent to transfer data to the transcluded content from the grandparent.
Angular/2 Solution
In Angular/2, this is easy to implement using template child content transferring from the parent and template outlet displaying in the child.
This example is taken from the excellent article on content projection in Angular/2 by Clarity Design System. Angular/2 documents are somewhat lack in this regard.
@Component({
selector: 'wrapper',
template: `
<div class="box" *ngFor="let item of items">
<ng-container [ngTemplateOutlet]="template; content: { item }"></ng-container>
</div> `
})
class Wrapper {
items = [0, 0, 0];
@ContentChild(TemplateRef) template: TemplateRef; }@Component({
selector: 'parrent',
template: `
<wrapper>
<ng-template>
{{ item.name }} - {{ item.amount }}
</ng-template>
</wrapper>
`
})
class Parent {}
Here, several things happens:
The parent transfer a template to the wrapper child by template projection
The child capture in a property and access the transferred template using @ContentChild content query.
Then the child uses the template inside an ngForOf loop using ngTemplateOutlet
Whats most important to notice here regarding our case is the transference of context into the projected template. This is how the child can give data to the projected template.
AngularJs Solution
This feature has already been asked before and was not dealt officially in AngularJs core.
It was shown that this can be done in augmented or derivative directive of ng-transclude . Excellent examples were given that others build upon.
The solution take the code of what ng-transclude does — which is essentially using the $transclude function to attach a content — and adding a logic to it that provide the transcluded content the scope of the child.
The main logic can be condensed to providing the $transclude function a base scope of our own choosing instead of the default one that $transclude is using which is the grandparent (the root) scope:
// const customScope = $scope (which is the parent) and not the grandparent$transclude(customScope, function( clone ) {
$element.empty();
$element.append( clone ); });
This instead of the default way that ng-transclude does it, which is to provide the transcluded content access to the a specialized scope getting the properties of the grandparent.
$transclude(ngTranscludeCloneAttachFn, null, slotName);
...
function ngTranscludeCloneAttachFn(clone, transcludedScope) {
...
$element.append(clone);
...
}
The API for the $transclude function is specified as:
$transclude — A transclude linking function pre-bound to the correct transclusion scope: function([scope], cloneLinkingFn, futureParentElement, slotName):
- scope: (optional) override the scope.
- cloneLinkingFn: (optional) argument to create clones of the original transcluded content.
- futureParentElement (optional):
defines the parent to which the cloneLinkingFn will add the cloned elements.
default: $element.parent() resp. $element for transclude:’element’ resp. transclude:true.
only needed for transcludes that are allowed to contain non html elements (e.g. SVG elements) and when the cloneLinkingFn is passed, as those elements need to created and cloned in a special way when they are defined outside their usual containers (e.g. like <svg>).
See also the directive.templateNamespace property.
- slotName: (optional) the name of the slot to transclude. If falsy (e.g. null, undefined or ‘’) then the default transclusion is provided. The $transclude function also has a method on it, $transclude.isSlotFilled(slotName), which returns true if the specified slot contains content (i.e. one or more DOM nodes).
Feature — Have Access to Both Parent and Grandparent Data
Those solutions can be built upon and add:
Explicit data binding to the transcluded content so the parent will have the option to provide the transcluded content only the data it wants to provide.
Allow the transcluded content access to the grandparent $scope as before — The same way it had using the regular
ng-transclude
.
We want to be able to give the transcluded content access to some data from the parent and to keep access to the scope of its declaration place — the grandparent
myAppModule.component('grandparent', {
template: `
<parent items="$ctrl.items>
<div>{{ firstName }}</div> // this is from the parent data
<div>{{ $ctrl.items.length }}</div> // this is from the grandparent
</parent>
`
...
});myAppModule.component('parent', {
template: `
<div ng-repeat="item in $ctrl.items">
<custom-transclude data="item"></custom-transclude>
</div>
`
...
});
NgRepeat as an Example
AngularJs already does something similar. In ng-repeat itself, we see some kind of this behavior. The ng-repeat acts as a parent, the container of the ng-repeat as a grandparent, and the grandparent specify to the ng-repeat the template to repeat. In that template — the grandson - it has access to:
Its own scope — the grandparent scope
Some explicit properties the
ng-repeat
gives it like:$index
,$last
,$first
and others. Most important, is thevalueIdentifier
specified in the dsl expressionmyItem in $ctrl.items
. The myItem is given to the transcluded content for each one with the key name specified in the expression:myItem
.
How does ng-repeat does this?
Looking at ng-repeat code, this can be seen:
var updateScope = function(scope, index, valueIdentifier, value,
keyIdentifier, key, arrayLength) {
scope[valueIdentifier] = value;
if (keyIdentifier) scope[keyIdentifier] = key;
scope.$index = index;
scope.$first = (index === 0);
scope.$last = (index === (arrayLength - 1));
scope.$middle = !(scope.$first || scope.$last);
scope.$odd = !(scope.$even = (index & 1) === 0); };...return {
restrict: 'A',
multiElement: true,
transclude: 'element',
priority: 1000,
terminal: true,
$$tlb: true,
compile: function ngRepeatCompile($element, $attr) {
return function ngRepeatLink($scope, $element, $attr, ctrl,
$transclude) { $scope.$watchCollection(rhs, function
ngRepeatAction(collection) {
...
// new item which we don't know about
$transclude(function ngRepeatTransclude(clone, scope) {
block.scope = scope;
...
updateScope(block.scope, index, valueIdentifier, value,
keyIdentifier, key, collectionLength);
});
});
}
}
...
};
Here it can be seen that ng-repeat create for each item in the list a DOM copy by using the transclusion function with a value for the cloneLinkFn parameter. The $transclude api specify that if you give a cloneLinkFn function, the $transclude create a copy of the transcluded content and not use it directly.
The second important thing to notice here, the $transclude function gives the cloneLinkFn the clone DOM, and a special generated scope it created.
That special generated scope is inheriting prototypical from the grandparent — where the transcluded content comes from — but is connected via the $child-$parent relationship to the scope of the parent where the transclude function is used — the ng-repeat. Meaning the DOM transcluded copy has access to the grandparent scope data, but it get $destroy message from the parent when it goes away. It does not however has any access to the parent scope data.
To get access to the parent scope data, the ng-repeat directive expicitly attach data to its generated scope. For example the $index , $last , $first data that we can see.
A Look Into NgTransclude
After ngRepeat
, How does ngTransclude
does it work? Looking at it’s code, this is what can be seen:
var ngTranscludeDirective = ['$compile', function($compile) {return {
restrict: 'EAC',
compile: function ngTranscludeCompile(tElement) {
return function ngTranscludePostLink($scope, $element, $attrs,
controller, $transclude) {
};
...
$transclude(ngTranscludeCloneAttachFn, null, slotName);
...
function ngTranscludeCloneAttachFn(clone, transcludedScope) {
...
$element.append(clone);
...
} }
}];
We can see almost the same usage of the $transclude facility. Creating a DOM copy of the transcluded content by providing a cloneAttachFunction
and adding that clone to the DOM.
Returning to our original quest, how can we have a directive that does a transclusion that keeps access to the grandparent data but allow giving the transcluded copy another data of our own also like ng-repeat
?
AngularJs/1 Augmented Transclude Directive
The solution is much simpler that anticipated.
Looking at the ngTransclude
code, all we have to do is:
Give it/Listen/Watch on a binding parameter context that we will use to give the directive a custom data.
Attach that given data to the generated scope that then clone transcluded DOM is attached to.
Here, the custom transclusion function does 2 things:
Watch over a directive attribute expression, gets it’s value and save it localy.
Get the transcluded clone generated special scope and save it localy.
Update the generated special scope with the custom data given to the directive of first time and each time it’s reference is updated.
return function ngTranscludePostLink(
...
) {
let context = null;
let childScope = null;
...
$scope.$watch($attrs.context, (newVal, oldVal) => {
context = newVal;
updateScope(childScope, context);
});
...
$transclude(ngTranscludeCloneAttachFn, null, slotName);
...
function ngTranscludeCloneAttachFn(clone, transcludedScope) {
...
$element.append(clone);
childScope = transcludedScope;
updateScope(childScope, context);
...
}
...
function updateScope(scope, varsHash) {
if (!scope || !varsHash) {
return;
} angular.extend(scope, varsHash);
}
}
Now, with the brand new cr-transclude directive, we can create our one list generice list component that accpect from the outside template how to show its rendered items.
App Component:
<my-list items="$ctrl.movies">
<div>App data: {{ $ctrl.header }}</div>
<div>Name:{{ name }} Year: {{ year }} Rating: {{ rating
}}</div>
</my-list>
MyList Component
<ul>
<li ng-repeat="item in $ctrl.items track by item.id">
<div>Ng repeat item scope id: {{ $id }}</div>
<cr-transclude context="item"></cr-transclude>
</li>
</ul>
Conclusion
This is how a semi template projection can be done in AngularJs/1. Adding a small logic to the original ngTransclude that gives it the power to transfer custom data from the parent to the transcluded content.
Many thanks to the people who contributed their knowledge and time in the GitHub issues, documents and articles given below. They were invaluable.
The custom directive is available here in GitHub and NPM.
References
- AngularJs directive $compile document
- AngularJs ng-transclude directive & code
- AngularJS ng-repeat directive & code
- Angular/2 ngTemplate outlet
- angular 1.2.18: ng-repeat problem with transclude
- ng-transclude should not create new sibling scope
- article - ng-content: The hidden docs
- opensource - ngTranscludeMode & fork for 1.5
- opensource - angular-directives-utils
Top comments (0)