Skip to content

Commit de26571

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 6a0686d commit de26571

File tree

2 files changed

+55
-12
lines changed

2 files changed

+55
-12
lines changed

src/ng/compile.js

+25-10
Original file line numberDiff line numberDiff line change
@@ -337,14 +337,15 @@
337337
* The contents are compiled and provided to the directive as a **transclusion function**. See the
338338
* {@link $compile#transclusion Transclusion} section below.
339339
*
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:
340+
* There are three kinds of transclusion depending upon whether you want to transclude just the contents of the
341+
* directive's element, the entire element or parts of the element:
342342
*
343343
* * `true` - transclude the content (i.e. the child nodes) of the directive's element.
344344
* * `'element'` - transclude the whole of the directive's element including any directives on this
345345
* element that defined at a lower priority than this directive. When used, the `template`
346346
* property is ignored.
347-
*
347+
* * **`{...}` (an object hash):** - map elements of the content onto transclusion "slots" in the template.
348+
* See {@link ngTransclude} for more information.
348349
*
349350
* #### `compile`
350351
*
@@ -1511,7 +1512,11 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
15111512
// so that they are available inside the `controllersBoundTransclude` function
15121513
var boundSlots = boundTranscludeFn.$$slots = createMap();
15131514
for (var slotName in transcludeFn.$$slots) {
1514-
boundSlots[slotName] = createBoundTranscludeFn(scope, transcludeFn.$$slots[slotName], previousBoundTranscludeFn);
1515+
if (transcludeFn.$$slots[slotName]) {
1516+
boundSlots[slotName] = createBoundTranscludeFn(scope, transcludeFn.$$slots[slotName], previousBoundTranscludeFn);
1517+
} else {
1518+
boundSlots[slotName] = null;
1519+
}
15151520
}
15161521

15171522
return boundTranscludeFn;
@@ -1870,7 +1875,10 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
18701875
var optional = (slotName.charAt(0) === '?');
18711876
slotName = optional ? slotName.substring(1) : slotName;
18721877
slotNames[key] = slotName;
1873-
slots[slotName] = [];
1878+
// We explicitly assign `null` since this implies that a slot was defined but not filled.
1879+
// Later when calling boundTransclusion functions with a slot name we only error if the
1880+
// slot is `undefined`
1881+
slots[slotName] = null;
18741882
// filledSlots contains `true` for all slots that are either optional or have been
18751883
// filled. This is used to check that we have not missed any required slots
18761884
filledSlots[slotName] = optional;
@@ -1881,6 +1889,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
18811889
var slotName = slotNames[directiveNormalize(nodeName_(node))];
18821890
if (slotName) {
18831891
filledSlots[slotName] = true;
1892+
slots[slotName] = slots[slotName] || [];
18841893
slots[slotName].push(node);
18851894
} else {
18861895
$template.push(node);
@@ -1894,9 +1903,12 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
18941903
}
18951904
});
18961905

1897-
forEach(Object.keys(slots), function(slotName) {
1898-
slots[slotName] = compilationGenerator(mightHaveMultipleTransclusionError, slots[slotName], transcludeFn);
1899-
});
1906+
for(var slotName in slots) {
1907+
if (slots[slotName]) {
1908+
// Only define a transclusion function if the slot was filled
1909+
slots[slotName] = compilationGenerator(mightHaveMultipleTransclusionError, slots[slotName], transcludeFn);
1910+
}
1911+
}
19001912
}
19011913

19021914
$compileNode.empty(); // clear contents
@@ -2125,6 +2137,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
21252137
// is later passed as `parentBoundTranscludeFn` to `publicLinkFn`
21262138
transcludeFn = controllersBoundTransclude;
21272139
transcludeFn.$$boundTransclude = boundTranscludeFn;
2140+
transcludeFn.$$slots = boundTranscludeFn.$$slots;
21282141
}
21292142

21302143
if (controllerDirectives) {
@@ -2222,13 +2235,15 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
22222235
}
22232236
if (slotName) {
22242237
var slotTranscludeFn = boundTranscludeFn.$$slots[slotName];
2225-
if (!slotTranscludeFn) {
2238+
if (slotTranscludeFn) {
2239+
return slotTranscludeFn(scope, cloneAttachFn, transcludeControllers, futureParentElement, scopeToChild);
2240+
}
2241+
if (isUndefined(slotTranscludeFn)) {
22262242
throw $compileMinErr('noslot',
22272243
'No parent directive that requires a transclusion with slot name "{0}". ' +
22282244
'Element: {1}',
22292245
slotName, startingTag($element));
22302246
}
2231-
return slotTranscludeFn(scope, cloneAttachFn, transcludeControllers, futureParentElement, scopeToChild);
22322247
}
22332248
return boundTranscludeFn(scope, cloneAttachFn, transcludeControllers, futureParentElement, scopeToChild);
22342249
}

test/ng/compileSpec.js

+30-2
Original file line numberDiff line numberDiff line change
@@ -7949,7 +7949,7 @@ describe('$compile', function() {
79497949
'</minion-component>')($rootScope);
79507950
$rootScope.$apply();
79517951

7952-
var minionTranscludeFn = capturedTranscludeFn.$$boundTransclude.$$slots['minionSlot'];
7952+
var minionTranscludeFn = capturedTranscludeFn.$$slots['minionSlot'];
79537953
var minions = minionTranscludeFn();
79547954
expect(minions[0].outerHTML).toEqual('<minion class="ng-scope">stuart</minion>');
79557955
expect(minions[1].outerHTML).toEqual('<minion class="ng-scope">bob</minion>');
@@ -7959,7 +7959,7 @@ describe('$compile', function() {
79597959
var minionScope = jqLite(minions[0]).scope();
79607960
expect(minionScope.$parent).toBe(scope);
79617961

7962-
var bossTranscludeFn = capturedTranscludeFn.$$boundTransclude.$$slots['bossSlot'];
7962+
var bossTranscludeFn = capturedTranscludeFn.$$slots['bossSlot'];
79637963
var boss = bossTranscludeFn();
79647964
expect(boss[0].outerHTML).toEqual('<boss class="ng-scope">gru</boss>');
79657965

@@ -7972,6 +7972,34 @@ describe('$compile', function() {
79727972
dealoc(minions);
79737973
});
79747974
});
7975+
7976+
it('should set unfilled optional transclude slots to `null` in the $transclude.$$slots property', function() {
7977+
var capturedTranscludeFn;
7978+
module(function() {
7979+
directive('minionComponent', function() {
7980+
return {
7981+
restrict: 'E',
7982+
scope: {},
7983+
transclude: {
7984+
minion: 'minionSlot',
7985+
boss: '?bossSlot'
7986+
},
7987+
link: function(s, e, a, c, transcludeFn) {
7988+
capturedTranscludeFn = transcludeFn;
7989+
}
7990+
};
7991+
});
7992+
});
7993+
inject(function($rootScope, $compile) {
7994+
element = $compile(
7995+
'<minion-component>' +
7996+
'<minion>stuart</minion>' +
7997+
'<span>dorothy</span>' +
7998+
'</minion-component>')($rootScope);
7999+
expect(capturedTranscludeFn.$$slots.minionSlot).toEqual(jasmine.any(Function));
8000+
expect(capturedTranscludeFn.$$slots.bossSlot).toBe(null);
8001+
});
8002+
});
79758003
});
79768004

79778005

0 commit comments

Comments
 (0)