Skip to content

Commit c3a2691

Browse files
fix($compile): swap keys and values for transclude definition object
Closes angular#13439
1 parent 6976d6d commit c3a2691

File tree

2 files changed

+100
-38
lines changed

2 files changed

+100
-38
lines changed

src/ng/compile.js

Lines changed: 54 additions & 20 deletions
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;
@@ -1855,32 +1868,42 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
18551868
} else {
18561869

18571870
var slots = createMap();
1871+
18581872
$template = jqLite(jqLiteClone(compileNode)).contents();
18591873

18601874
if (isObject(directiveValue)) {
18611875

1862-
// We have transclusion slots - collect them up and compile them and store their
1863-
// transclusion functions
1876+
// We have transclusion slots,
1877+
// collect them up, compile them and store their transclusion functions
18641878
$template = [];
1865-
var slotNames = createMap();
1879+
1880+
var slotMap = createMap();
18661881
var filledSlots = createMap();
18671882

1868-
// Parse the slot names: if they start with a ? then they are optional
1869-
forEach(directiveValue, function(slotName, key) {
1870-
var optional = (slotName.charAt(0) === '?');
1871-
slotName = optional ? slotName.substring(1) : slotName;
1872-
slotNames[key] = slotName;
1873-
slots[slotName] = [];
1883+
// Parse the element selectors
1884+
forEach(directiveValue, function(elementSelector, slotName) {
1885+
// If an element selector starts with a ? then it is optional
1886+
var optional = (elementSelector.charAt(0) === '?');
1887+
elementSelector = optional ? elementSelector.substring(1) : elementSelector;
1888+
1889+
slotMap[elementSelector] = slotName;
1890+
1891+
// We explicitly assign `null` since this implies that a slot was defined but not filled.
1892+
// Later when calling boundTransclusion functions with a slot name we only error if the
1893+
// slot is `undefined`
1894+
slots[slotName] = null;
1895+
18741896
// filledSlots contains `true` for all slots that are either optional or have been
18751897
// filled. This is used to check that we have not missed any required slots
18761898
filledSlots[slotName] = optional;
18771899
});
18781900

18791901
// Add the matching elements into their slot
18801902
forEach($compileNode.contents(), function(node) {
1881-
var slotName = slotNames[directiveNormalize(nodeName_(node))];
1903+
var slotName = slotMap[nodeName_(node)];
18821904
if (slotName) {
18831905
filledSlots[slotName] = true;
1906+
slots[slotName] = slots[slotName] || [];
18841907
slots[slotName].push(node);
18851908
} else {
18861909
$template.push(node);
@@ -1894,9 +1917,12 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
18941917
}
18951918
});
18961919

1897-
forEach(Object.keys(slots), function(slotName) {
1898-
slots[slotName] = compilationGenerator(mightHaveMultipleTransclusionError, slots[slotName], transcludeFn);
1899-
});
1920+
for (var slotName in slots) {
1921+
if (slots[slotName]) {
1922+
// Only define a transclusion function if the slot was filled
1923+
slots[slotName] = compilationGenerator(mightHaveMultipleTransclusionError, slots[slotName], transcludeFn);
1924+
}
1925+
}
19001926
}
19011927

19021928
$compileNode.empty(); // clear contents
@@ -2125,6 +2151,8 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
21252151
// is later passed as `parentBoundTranscludeFn` to `publicLinkFn`
21262152
transcludeFn = controllersBoundTransclude;
21272153
transcludeFn.$$boundTransclude = boundTranscludeFn;
2154+
// expose the slots on the `$transclude` function
2155+
transcludeFn.$slots = boundTranscludeFn.$$slots;
21282156
}
21292157

21302158
if (controllerDirectives) {
@@ -2221,16 +2249,22 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
22212249
futureParentElement = hasElementTranscludeDirective ? $element.parent() : $element;
22222250
}
22232251
if (slotName) {
2252+
// slotTranscludeFn can be one of three things:
2253+
// * a transclude function - a filled slot
2254+
// * `null` - an optional slot that was not filled
2255+
// * `undefined` - a slot that was not declared (i.e. invalid)
22242256
var slotTranscludeFn = boundTranscludeFn.$$slots[slotName];
2225-
if (!slotTranscludeFn) {
2257+
if (slotTranscludeFn) {
2258+
return slotTranscludeFn(scope, cloneAttachFn, transcludeControllers, futureParentElement, scopeToChild);
2259+
} else if (isUndefined(slotTranscludeFn)) {
22262260
throw $compileMinErr('noslot',
22272261
'No parent directive that requires a transclusion with slot name "{0}". ' +
22282262
'Element: {1}',
22292263
slotName, startingTag($element));
22302264
}
2231-
return slotTranscludeFn(scope, cloneAttachFn, transcludeControllers, futureParentElement, scopeToChild);
2265+
} else {
2266+
return boundTranscludeFn(scope, cloneAttachFn, transcludeControllers, futureParentElement, scopeToChild);
22322267
}
2233-
return boundTranscludeFn(scope, cloneAttachFn, transcludeControllers, futureParentElement, scopeToChild);
22342268
}
22352269
}
22362270
}

test/ng/compileSpec.js

Lines changed: 46 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7660,7 +7660,7 @@ describe('$compile', function() {
76607660
restrict: 'E',
76617661
scope: {},
76627662
transclude: {
7663-
boss: 'bossSlot'
7663+
bossSlot: 'boss'
76647664
},
76657665
template:
76667666
'<div class="other" ng-transclude></div>'
@@ -7722,7 +7722,7 @@ describe('$compile', function() {
77227722
restrict: 'E',
77237723
scope: {},
77247724
transclude: {
7725-
boss: 'bossSlot'
7725+
bossSlot: 'boss'
77267726
},
77277727
template:
77287728
'<div class="other" ng-transclude></div>'
@@ -7751,8 +7751,8 @@ describe('$compile', function() {
77517751
restrict: 'E',
77527752
scope: {},
77537753
transclude: {
7754-
minion: 'minionSlot',
7755-
boss: 'bossSlot'
7754+
minionSlot: 'minion',
7755+
bossSlot: 'boss'
77567756
},
77577757
template:
77587758
'<div class="boss" ng-transclude="bossSlot"></div>' +
@@ -7784,8 +7784,8 @@ describe('$compile', function() {
77847784
restrict: 'E',
77857785
scope: {},
77867786
transclude: {
7787-
minion: 'minionSlot',
7788-
boss: 'bossSlot'
7787+
minionSlot: 'minion',
7788+
bossSlot: 'boss'
77897789
},
77907790
template:
77917791
'<ng-transclude class="boss" ng-transclude-slot="bossSlot"></ng-transclude>' +
@@ -7816,8 +7816,8 @@ describe('$compile', function() {
78167816
restrict: 'E',
78177817
scope: {},
78187818
transclude: {
7819-
minion: 'minionSlot',
7820-
boss: 'bossSlot'
7819+
minionSlot: 'minion',
7820+
bossSlot: 'boss'
78217821
},
78227822
template:
78237823
'<div class="boss" ng-transclude="bossSlot"></div>' +
@@ -7845,8 +7845,8 @@ describe('$compile', function() {
78457845
restrict: 'E',
78467846
scope: {},
78477847
transclude: {
7848-
minion: 'minionSlot',
7849-
boss: '?bossSlot'
7848+
minionSlot: 'minion',
7849+
bossSlot: '?boss'
78507850
},
78517851
template:
78527852
'<div class="boss" ng-transclude="bossSlot"></div>' +
@@ -7875,7 +7875,7 @@ describe('$compile', function() {
78757875
restrict: 'E',
78767876
scope: {},
78777877
transclude: {
7878-
minion: 'minionSlot'
7878+
minionSlot: 'minion'
78797879
},
78807880
template:
78817881
'<div class="boss" ng-transclude="bossSlot"></div>' +
@@ -7923,14 +7923,14 @@ describe('$compile', function() {
79237923
});
79247924

79257925

7926-
it('should match against the normalized form of the element', function() {
7926+
it('should not normalize the element name', function() {
79277927
module(function() {
79287928
directive('foo', function() {
79297929
return {
79307930
restrict: 'E',
79317931
scope: {},
79327932
transclude: {
7933-
fooBar: 'fooBarSlot'
7933+
fooBarSlot: 'foo-bar'
79347934
},
79357935
template:
79367936
'<div class="other" ng-transclude="fooBarSlot"></div>'
@@ -7948,16 +7948,16 @@ 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() {
79557955
return {
79567956
restrict: 'E',
79577957
scope: {},
79587958
transclude: {
7959-
minion: 'minionSlot',
7960-
boss: 'bossSlot'
7959+
minionSlot: 'minion',
7960+
bossSlot: 'boss'
79617961
},
79627962
template:
79637963
'<div class="boss" ng-transclude="bossSlot"></div>' +
@@ -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+
minionSlot: 'minion',
8015+
bossSlot: '?boss'
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)