Skip to content

Commit 7f3d5f9

Browse files
feat($compile): expose transclusion $slots on the $transclude function
Unfilled optional slots will exist as properties of `$slots` but have a value of `null`. Closes angular#13426
1 parent 6976d6d commit 7f3d5f9

File tree

2 files changed

+70
-14
lines changed

2 files changed

+70
-14
lines changed

src/ng/compile.js

+39-11
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,8 @@
237237
* as those elements need to created and cloned in a special way when they are defined outside their
238238
* usual containers (e.g. like `<svg>`).
239239
* * See also the `directive.templateNamespace` property.
240-
*
240+
* The `$transclude` function has a property called `$slots`, which is a hash of slot names to slot transclusion
241+
* functions. If a slot was declared but not filled its value on the `$slots` object will be `null`.
241242
*
242243
* #### `require`
243244
* Require another directive and inject its controller as the fourth argument to the linking function. The
@@ -337,14 +338,22 @@
337338
* The contents are compiled and provided to the directive as a **transclusion function**. See the
338339
* {@link $compile#transclusion Transclusion} section below.
339340
*
340-
* There are two kinds of transclusion depending upon whether you want to transclude just the contents of the
341-
* directive's element or the entire element:
341+
* There are three kinds of transclusion depending upon whether you want to transclude just the contents of the
342+
* directive's element, the entire element or parts of the element:
342343
*
343344
* * `true` - transclude the content (i.e. the child nodes) of the directive's element.
344345
* * `'element'` - transclude the whole of the directive's element including any directives on this
345346
* element that defined at a lower priority than this directive. When used, the `template`
346347
* property is ignored.
348+
* * **`{...}` (an object hash):** - map elements of the content onto transclusion "slots" in the template.
349+
* See {@link ngTransclude} for more information.
350+
*
351+
* Mult-slot transclusion is declared by providing an object for the `transclude` property.
352+
* This object is a map where the keys are the canonical name of HTML elements to match in the transcluded HTML,
353+
* and the values are the names of the slots. If the name is prefixed with a `?` then that slot is optional.
347354
*
355+
* The slots are made available as `$transclude.$slots` on the transclude function that is passed to the
356+
* linking functions as the fifth parameter, and can be injected into the directive controller.
348357
*
349358
* #### `compile`
350359
*
@@ -1511,7 +1520,11 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
15111520
// so that they are available inside the `controllersBoundTransclude` function
15121521
var boundSlots = boundTranscludeFn.$$slots = createMap();
15131522
for (var slotName in transcludeFn.$$slots) {
1514-
boundSlots[slotName] = createBoundTranscludeFn(scope, transcludeFn.$$slots[slotName], previousBoundTranscludeFn);
1523+
if (transcludeFn.$$slots[slotName]) {
1524+
boundSlots[slotName] = createBoundTranscludeFn(scope, transcludeFn.$$slots[slotName], previousBoundTranscludeFn);
1525+
} else {
1526+
boundSlots[slotName] = null;
1527+
}
15151528
}
15161529

15171530
return boundTranscludeFn;
@@ -1870,7 +1883,10 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
18701883
var optional = (slotName.charAt(0) === '?');
18711884
slotName = optional ? slotName.substring(1) : slotName;
18721885
slotNames[key] = slotName;
1873-
slots[slotName] = [];
1886+
// We explicitly assign `null` since this implies that a slot was defined but not filled.
1887+
// Later when calling boundTransclusion functions with a slot name we only error if the
1888+
// slot is `undefined`
1889+
slots[slotName] = null;
18741890
// filledSlots contains `true` for all slots that are either optional or have been
18751891
// filled. This is used to check that we have not missed any required slots
18761892
filledSlots[slotName] = optional;
@@ -1881,6 +1897,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
18811897
var slotName = slotNames[directiveNormalize(nodeName_(node))];
18821898
if (slotName) {
18831899
filledSlots[slotName] = true;
1900+
slots[slotName] = slots[slotName] || [];
18841901
slots[slotName].push(node);
18851902
} else {
18861903
$template.push(node);
@@ -1894,9 +1911,12 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
18941911
}
18951912
});
18961913

1897-
forEach(Object.keys(slots), function(slotName) {
1898-
slots[slotName] = compilationGenerator(mightHaveMultipleTransclusionError, slots[slotName], transcludeFn);
1899-
});
1914+
for (var slotName in slots) {
1915+
if (slots[slotName]) {
1916+
// Only define a transclusion function if the slot was filled
1917+
slots[slotName] = compilationGenerator(mightHaveMultipleTransclusionError, slots[slotName], transcludeFn);
1918+
}
1919+
}
19001920
}
19011921

19021922
$compileNode.empty(); // clear contents
@@ -2125,6 +2145,8 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
21252145
// is later passed as `parentBoundTranscludeFn` to `publicLinkFn`
21262146
transcludeFn = controllersBoundTransclude;
21272147
transcludeFn.$$boundTransclude = boundTranscludeFn;
2148+
// expose the slots on the `$transclude` function
2149+
transcludeFn.$slots = boundTranscludeFn.$$slots;
21282150
}
21292151

21302152
if (controllerDirectives) {
@@ -2221,16 +2243,22 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
22212243
futureParentElement = hasElementTranscludeDirective ? $element.parent() : $element;
22222244
}
22232245
if (slotName) {
2246+
// slotTranscludeFn can be one of three things:
2247+
// * a transclude function - a filled slot
2248+
// * `null` - an optional slot that was not filled
2249+
// * `undefined` - a slot that was not declared (i.e. invalid)
22242250
var slotTranscludeFn = boundTranscludeFn.$$slots[slotName];
2225-
if (!slotTranscludeFn) {
2251+
if (slotTranscludeFn) {
2252+
return slotTranscludeFn(scope, cloneAttachFn, transcludeControllers, futureParentElement, scopeToChild);
2253+
} else if (isUndefined(slotTranscludeFn)) {
22262254
throw $compileMinErr('noslot',
22272255
'No parent directive that requires a transclusion with slot name "{0}". ' +
22282256
'Element: {1}',
22292257
slotName, startingTag($element));
22302258
}
2231-
return slotTranscludeFn(scope, cloneAttachFn, transcludeControllers, futureParentElement, scopeToChild);
2259+
} else {
2260+
return boundTranscludeFn(scope, cloneAttachFn, transcludeControllers, futureParentElement, scopeToChild);
22322261
}
2233-
return boundTranscludeFn(scope, cloneAttachFn, transcludeControllers, futureParentElement, scopeToChild);
22342262
}
22352263
}
22362264
}

test/ng/compileSpec.js

+31-3
Original file line numberDiff line numberDiff line change
@@ -7948,7 +7948,7 @@ describe('$compile', function() {
79487948
});
79497949

79507950

7951-
it('should provide the elements marked with matching transclude elements as additional transclude functions on the $$slots property', function() {
7951+
it('should provide the elements marked with matching transclude elements as additional transclude functions on the $slots property', function() {
79527952
var capturedTranscludeFn;
79537953
module(function() {
79547954
directive('minionComponent', function() {
@@ -7979,7 +7979,7 @@ describe('$compile', function() {
79797979
'</minion-component>')($rootScope);
79807980
$rootScope.$apply();
79817981

7982-
var minionTranscludeFn = capturedTranscludeFn.$$boundTransclude.$$slots['minionSlot'];
7982+
var minionTranscludeFn = capturedTranscludeFn.$slots['minionSlot'];
79837983
var minions = minionTranscludeFn();
79847984
expect(minions[0].outerHTML).toEqual('<minion class="ng-scope">stuart</minion>');
79857985
expect(minions[1].outerHTML).toEqual('<minion class="ng-scope">bob</minion>');
@@ -7989,7 +7989,7 @@ describe('$compile', function() {
79897989
var minionScope = jqLite(minions[0]).scope();
79907990
expect(minionScope.$parent).toBe(scope);
79917991

7992-
var bossTranscludeFn = capturedTranscludeFn.$$boundTransclude.$$slots['bossSlot'];
7992+
var bossTranscludeFn = capturedTranscludeFn.$slots['bossSlot'];
79937993
var boss = bossTranscludeFn();
79947994
expect(boss[0].outerHTML).toEqual('<boss class="ng-scope">gru</boss>');
79957995

@@ -8002,6 +8002,34 @@ describe('$compile', function() {
80028002
dealoc(minions);
80038003
});
80048004
});
8005+
8006+
it('should set unfilled optional transclude slots to `null` in the $transclude.$slots property', function() {
8007+
var capturedTranscludeFn;
8008+
module(function() {
8009+
directive('minionComponent', function() {
8010+
return {
8011+
restrict: 'E',
8012+
scope: {},
8013+
transclude: {
8014+
minion: 'minionSlot',
8015+
boss: '?bossSlot'
8016+
},
8017+
link: function(s, e, a, c, transcludeFn) {
8018+
capturedTranscludeFn = transcludeFn;
8019+
}
8020+
};
8021+
});
8022+
});
8023+
inject(function($rootScope, $compile) {
8024+
element = $compile(
8025+
'<minion-component>' +
8026+
'<minion>stuart</minion>' +
8027+
'<span>dorothy</span>' +
8028+
'</minion-component>')($rootScope);
8029+
expect(capturedTranscludeFn.$slots.minionSlot).toEqual(jasmine.any(Function));
8030+
expect(capturedTranscludeFn.$slots.bossSlot).toBe(null);
8031+
});
8032+
});
80058033
});
80068034

80078035

0 commit comments

Comments
 (0)