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

Commit 0812af4

Browse files
feat(ngTransclude): don't overwrite the contents with an unfilled optional slot
Previously the contents of the `ngTransclude` element would always be emptied, even if there was no transclusion to replace it. Now, optional slots that have not been filled with content will not cause the `ngTransclude` contents to be emptied. Closes #13426
1 parent c3ae6ed commit 0812af4

File tree

3 files changed

+59
-34
lines changed

3 files changed

+59
-34
lines changed

src/ng/compile.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -2152,7 +2152,9 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
21522152
transcludeFn = controllersBoundTransclude;
21532153
transcludeFn.$$boundTransclude = boundTranscludeFn;
21542154
// expose the slots on the `$transclude` function
2155-
transcludeFn.$slots = boundTranscludeFn.$$slots;
2155+
transcludeFn.isSlotFilled = function(slotName) {
2156+
return !!boundTranscludeFn.$$slots[slotName];
2157+
};
21562158
}
21572159

21582160
if (controllerDirectives) {

src/ng/directive/ngTransclude.js

+10-4
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,9 @@
1111
* You can specify that you want to insert a named transclusion slot, instead of the default slot, by providing the slot name
1212
* as the value of the `ng-transclude` or `ng-transclude-slot` attribute.
1313
*
14-
* Any existing content of this element will be removed before the transcluded content is inserted,
15-
* but only if the transcluded content is not empty.
14+
* For required slots and the default transclusion, existing content will be removed before the transcluded content is inserted.
15+
*
16+
* For optional slots, existing content is left in place, if the slot was not filled.
1617
*
1718
* @element ANY
1819
*
@@ -118,11 +119,13 @@
118119
restrict: 'E',
119120
transclude: {
120121
'paneTitle': '?title',
121-
'paneBody': 'body'
122+
'paneBody': 'body',
123+
'paneFooter': '?footer'
122124
},
123125
template: '<div style="border: 1px solid black;">' +
124126
'<div ng-transclude="title" style="background-color: gray"></div>' +
125127
'<div ng-transclude="body"></div>' +
128+
'<div ng-transclude="footer" style="background-color: gray">Default Footer</div>' +
126129
'</div>'
127130
};
128131
})
@@ -171,7 +174,10 @@ var ngTranscludeDirective = ngDirective({
171174
startingTag($element));
172175
}
173176

174-
$transclude(ngTranscludeCloneAttachFn, null, $attrs.ngTransclude || $attrs.ngTranscludeSlot);
177+
// If there is no slot name defined or the slot name is not optional
178+
// then transclude the slot
179+
var slotName = $attrs.ngTransclude || $attrs.ngTranscludeSlot;
180+
$transclude(ngTranscludeCloneAttachFn, null, slotName);
175181
}
176182
});
177183

test/ng/compileSpec.js

+46-29
Original file line numberDiff line numberDiff line change
@@ -7966,7 +7966,7 @@ describe('$compile', function() {
79667966
});
79677967

79687968

7969-
it('should provide the elements marked with matching transclude elements as additional transclude functions on the $slots property', function() {
7969+
it('should return true from `isSlotFilled(slotName) for slots that have content in the transclusion', function() {
79707970
var capturedTranscludeFn;
79717971
module(function() {
79727972
directive('minionComponent', function() {
@@ -7975,7 +7975,7 @@ describe('$compile', function() {
79757975
scope: {},
79767976
transclude: {
79777977
minionSlot: 'minion',
7978-
bossSlot: 'boss'
7978+
bossSlot: '?boss'
79797979
},
79807980
template:
79817981
'<div class="boss" ng-transclude="bossSlot"></div>' +
@@ -7993,36 +7993,49 @@ describe('$compile', function() {
79937993
' <minion>stuart</minion>' +
79947994
' <minion>bob</minion>' +
79957995
' <span>dorothy</span>' +
7996-
' <boss>gru</boss>' +
79977996
'</minion-component>')($rootScope);
79987997
$rootScope.$apply();
79997998

8000-
var minionTranscludeFn = capturedTranscludeFn.$slots['minionSlot'];
8001-
var minions = minionTranscludeFn();
8002-
expect(minions[0].outerHTML).toEqual('<minion class="ng-scope">stuart</minion>');
8003-
expect(minions[1].outerHTML).toEqual('<minion class="ng-scope">bob</minion>');
8004-
8005-
var scope = element.scope();
8006-
8007-
var minionScope = jqLite(minions[0]).scope();
8008-
expect(minionScope.$parent).toBe(scope);
8009-
8010-
var bossTranscludeFn = capturedTranscludeFn.$slots['bossSlot'];
8011-
var boss = bossTranscludeFn();
8012-
expect(boss[0].outerHTML).toEqual('<boss class="ng-scope">gru</boss>');
7999+
var hasMinions = capturedTranscludeFn.isSlotFilled('minionSlot');
8000+
var hasBosses = capturedTranscludeFn.isSlotFilled('bossSlot');
80138001

8014-
var bossScope = jqLite(boss[0]).scope();
8015-
expect(bossScope.$parent).toBe(scope);
8016-
8017-
expect(bossScope).not.toBe(minionScope);
8002+
expect(hasMinions).toBe(true);
8003+
expect(hasBosses).toBe(false);
8004+
});
8005+
});
80188006

8019-
dealoc(boss);
8020-
dealoc(minions);
8007+
it('should not overwrite the contents of an `ng-transclude` element, if the matching optional slot is not filled', function() {
8008+
module(function() {
8009+
directive('minionComponent', function() {
8010+
return {
8011+
restrict: 'E',
8012+
scope: {},
8013+
transclude: {
8014+
minionSlot: 'minion',
8015+
bossSlot: '?boss'
8016+
},
8017+
template:
8018+
'<div class="boss" ng-transclude="bossSlot">default boss content</div>' +
8019+
'<div class="minion" ng-transclude="minionSlot">default minion content</div>' +
8020+
'<div class="other" ng-transclude>default content</div>'
8021+
};
8022+
});
8023+
});
8024+
inject(function($rootScope, $compile) {
8025+
element = $compile(
8026+
'<minion-component>' +
8027+
'<minion>stuart</minion>' +
8028+
'<span>dorothy</span>' +
8029+
'<minion>kevin</minion>' +
8030+
'</minion-component>')($rootScope);
8031+
$rootScope.$apply();
8032+
expect(element.children().eq(0).text()).toEqual('default boss content');
8033+
expect(element.children().eq(1).text()).toEqual('stuartkevin');
8034+
expect(element.children().eq(2).text()).toEqual('dorothy');
80218035
});
80228036
});
80238037

8024-
it('should set unfilled optional transclude slots to `null` in the $transclude.$slots property', function() {
8025-
var capturedTranscludeFn;
8038+
it('should not overwrite the contents of an `ng-transclude` element, if the matching optional slot is not filled', function() {
80268039
module(function() {
80278040
directive('minionComponent', function() {
80288041
return {
@@ -8032,9 +8045,10 @@ describe('$compile', function() {
80328045
minionSlot: 'minion',
80338046
bossSlot: '?boss'
80348047
},
8035-
link: function(s, e, a, c, transcludeFn) {
8036-
capturedTranscludeFn = transcludeFn;
8037-
}
8048+
template:
8049+
'<div class="boss" ng-transclude="bossSlot">default boss content</div>' +
8050+
'<div class="minion" ng-transclude="minionSlot">default minion content</div>' +
8051+
'<div class="other" ng-transclude>default content</div>'
80388052
};
80398053
});
80408054
});
@@ -8043,9 +8057,12 @@ describe('$compile', function() {
80438057
'<minion-component>' +
80448058
'<minion>stuart</minion>' +
80458059
'<span>dorothy</span>' +
8060+
'<minion>kevin</minion>' +
80468061
'</minion-component>')($rootScope);
8047-
expect(capturedTranscludeFn.$slots.minionSlot).toEqual(jasmine.any(Function));
8048-
expect(capturedTranscludeFn.$slots.bossSlot).toBe(null);
8062+
$rootScope.$apply();
8063+
expect(element.children().eq(0).text()).toEqual('default boss content');
8064+
expect(element.children().eq(1).text()).toEqual('stuartkevin');
8065+
expect(element.children().eq(2).text()).toEqual('dorothy');
80498066
});
80508067
});
80518068
});

0 commit comments

Comments
 (0)