Skip to content
This repository was archived by the owner on Apr 12, 2024. It is now read-only.

Commit a4ada8b

Browse files
feat($compile): multiple transclusion via named slots
Now you can efficiently split up and transclude content into specified places in a component's template. ```html <pane> <pane-title>Some content for slot A</pane-title> <pane-content>Some content for slot A</pane-content> </component> ``` ```js mod.directive('pane', function() { return { restrict: 'E', transclude: { paneTitle: '?titleSlot', paneContent: 'contentSlot' }, template: '<div class="pane">' + '<h1 ng-transclude="titleSlot"></h1>' + '<div ng-transclude="contentSlot"></div>' + '</div>' + }; }); ``` Closes #4357 Closes #12742 Closes #11736 Closes #12934
1 parent 40c974a commit a4ada8b

File tree

3 files changed

+421
-9
lines changed

3 files changed

+421
-9
lines changed

src/ng/compile.js

+66-2
Original file line numberDiff line numberDiff line change
@@ -1480,6 +1480,13 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
14801480
});
14811481
};
14821482

1483+
// We need to attach the transclusion slots onto the `boundTranscludeFn`
1484+
// so that they are available inside the `controllersBoundTransclude` function
1485+
var boundSlots = boundTranscludeFn.$$slots = createMap();
1486+
for (var slotName in transcludeFn.$$slots) {
1487+
boundSlots[slotName] = createBoundTranscludeFn(scope, transcludeFn.$$slots[slotName], previousBoundTranscludeFn);
1488+
}
1489+
14831490
return boundTranscludeFn;
14841491
}
14851492

@@ -1821,9 +1828,56 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
18211828
nonTlbTranscludeDirective: nonTlbTranscludeDirective
18221829
});
18231830
} else {
1831+
1832+
var slots = createMap();
18241833
$template = jqLite(jqLiteClone(compileNode)).contents();
1834+
1835+
if (isObject(directiveValue)) {
1836+
1837+
// We have transclusion slots - collect them up and compile them and store their
1838+
// transclusion functions
1839+
$template = [];
1840+
var slotNames = createMap();
1841+
var filledSlots = createMap();
1842+
1843+
// Parse the slot names: if they start with a ? then they are optional
1844+
forEach(directiveValue, function(slotName, key) {
1845+
var optional = (slotName.charAt(0) === '?');
1846+
slotName = optional ? slotName.substring(1) : slotName;
1847+
slotNames[key] = slotName;
1848+
slots[slotName] = [];
1849+
// filledSlots contains `true` for all slots that are either optional or have been
1850+
// filled. This is used to check that we have not missed any required slots
1851+
filledSlots[slotName] = optional;
1852+
});
1853+
1854+
// Add the matching elements into their slot
1855+
forEach($compileNode.children(), function(node) {
1856+
var slotName = slotNames[directiveNormalize(nodeName_(node))];
1857+
var slot = $template;
1858+
if (slotName) {
1859+
filledSlots[slotName] = true;
1860+
slots[slotName].push(node);
1861+
} else {
1862+
$template.push(node);
1863+
}
1864+
});
1865+
1866+
// Check for required slots that were not filled
1867+
forEach(filledSlots, function(filled, slotName) {
1868+
if (!filled) {
1869+
throw $compileMinErr('reqslot', 'Required transclusion slot `{0}` was not filled.', slotName);
1870+
}
1871+
});
1872+
1873+
forEach(Object.keys(slots), function(slotName) {
1874+
slots[slotName] = compilationGenerator(mightHaveMultipleTransclusionError, slots[slotName], transcludeFn);
1875+
});
1876+
}
1877+
18251878
$compileNode.empty(); // clear contents
18261879
childTranscludeFn = compilationGenerator(mightHaveMultipleTransclusionError, $template, transcludeFn);
1880+
childTranscludeFn.$$slots = slots;
18271881
}
18281882
}
18291883

@@ -2130,11 +2184,11 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
21302184

21312185
// This is the function that is injected as `$transclude`.
21322186
// Note: all arguments are optional!
2133-
function controllersBoundTransclude(scope, cloneAttachFn, futureParentElement) {
2187+
function controllersBoundTransclude(scope, cloneAttachFn, futureParentElement, slotName) {
21342188
var transcludeControllers;
2135-
21362189
// No scope passed in:
21372190
if (!isScope(scope)) {
2191+
slotName = futureParentElement;
21382192
futureParentElement = cloneAttachFn;
21392193
cloneAttachFn = scope;
21402194
scope = undefined;
@@ -2146,6 +2200,16 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
21462200
if (!futureParentElement) {
21472201
futureParentElement = hasElementTranscludeDirective ? $element.parent() : $element;
21482202
}
2203+
if (slotName) {
2204+
var slotTranscludeFn = boundTranscludeFn.$$slots[slotName];
2205+
if (!slotTranscludeFn) {
2206+
throw $compileMinErr('noslot',
2207+
'No parent directive that requires a transclusion with slot name "{0}". ' +
2208+
'Element: {1}',
2209+
slotName, startingTag($element));
2210+
}
2211+
return slotTranscludeFn(scope, cloneAttachFn, transcludeControllers, futureParentElement, scopeToChild);
2212+
}
21492213
return boundTranscludeFn(scope, cloneAttachFn, transcludeControllers, futureParentElement, scopeToChild);
21502214
}
21512215
}

src/ng/directive/ngTransclude.js

+66-7
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,20 @@
88
* @description
99
* Directive that marks the insertion point for the transcluded DOM of the nearest parent directive that uses transclusion.
1010
*
11+
* You can specify that you want to insert a named transclusion slot, instead of the default slot, by providing the slot name
12+
* as the value of the `ng-transclude` or `ng-transclude-slot` attribute.
13+
*
1114
* Any existing content of the element that this directive is placed on will be removed before the transcluded content is inserted.
1215
*
1316
* @element ANY
1417
*
18+
* @param {string} ngTransclude|ngTranscludeSlot the name of the slot to insert at this point. If this is not provided or empty then
19+
* the default slot is used.
20+
*
1521
* @example
16-
<example module="transcludeExample">
22+
* ### Default transclusion
23+
* This example demonstrates simple transclusion.
24+
<example name="simpleTranscludeExample" module="transcludeExample">
1725
<file name="index.html">
1826
<script>
1927
angular.module('transcludeExample', [])
@@ -53,21 +61,72 @@
5361
</file>
5462
</example>
5563
*
56-
*/
64+
* @example
65+
* ### Multi-slot transclusion
66+
<example name="multiSlotTranscludeExample" module="multiSlotTranscludeExample">
67+
<file name="index.html">
68+
<div ng-controller="ExampleController">
69+
<input ng-model="title" aria-label="title"> <br/>
70+
<textarea ng-model="text" aria-label="text"></textarea> <br/>
71+
<pane>
72+
<pane-title><a ng-href="{{link}}">{{title}}</a></pane-title>
73+
<pane-body><p>{{text}}</p></pane-body>
74+
</pane>
75+
</div>
76+
</file>
77+
<file name="app.js">
78+
angular.module('multiSlotTranscludeExample', [])
79+
.directive('pane', function(){
80+
return {
81+
restrict: 'E',
82+
transclude: {
83+
'paneTitle': '?title',
84+
'paneBody': 'body'
85+
},
86+
template: '<div style="border: 1px solid black;">' +
87+
'<div ng-transclude="title" style="background-color: gray"></div>' +
88+
'<div ng-transclude="body"></div>' +
89+
'</div>'
90+
};
91+
})
92+
.controller('ExampleController', ['$scope', function($scope) {
93+
$scope.title = 'Lorem Ipsum';
94+
$scope.link = "https://google.com";
95+
$scope.text = 'Neque porro quisquam est qui dolorem ipsum quia dolor...';
96+
}]);
97+
</file>
98+
<file name="protractor.js" type="protractor">
99+
it('should have transcluded the title and the body', function() {
100+
var titleElement = element(by.model('title'));
101+
titleElement.clear();
102+
titleElement.sendKeys('TITLE');
103+
var textElement = element(by.model('text'));
104+
textElement.clear();
105+
textElement.sendKeys('TEXT');
106+
expect(element(by.binding('title')).getText()).toEqual('TITLE');
107+
expect(element(by.binding('text')).getText()).toEqual('TEXT');
108+
});
109+
</file>
110+
</example> */
111+
var ngTranscludeMinErr = minErr('ngTransclude');
57112
var ngTranscludeDirective = ngDirective({
58113
restrict: 'EAC',
59114
link: function($scope, $element, $attrs, controller, $transclude) {
115+
116+
function ngTranscludeCloneAttachFn(clone) {
117+
$element.empty();
118+
$element.append(clone);
119+
}
120+
60121
if (!$transclude) {
61-
throw minErr('ngTransclude')('orphan',
122+
throw ngTranscludeMinErr('orphan',
62123
'Illegal use of ngTransclude directive in the template! ' +
63124
'No parent directive that requires a transclusion found. ' +
64125
'Element: {0}',
65126
startingTag($element));
66127
}
67128

68-
$transclude(function(clone) {
69-
$element.empty();
70-
$element.append(clone);
71-
});
129+
$transclude(ngTranscludeCloneAttachFn, null, $attrs.ngTransclude || $attrs.ngTranscludeSlot);
72130
}
73131
});
132+

0 commit comments

Comments
 (0)