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

Commit c405f88

Browse files
fix(ngTransclude): ensure that fallback content is compiled and linked correctly
Closes #14787
1 parent 159a68e commit c405f88

File tree

2 files changed

+132
-72
lines changed

2 files changed

+132
-72
lines changed

src/ng/directive/ngTransclude.js

Lines changed: 45 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -163,41 +163,56 @@ var ngTranscludeDirective = ['$compile', function($compile) {
163163
return {
164164
restrict: 'EAC',
165165
terminal: true,
166-
link: function($scope, $element, $attrs, controller, $transclude) {
167-
if ($attrs.ngTransclude === $attrs.$attr.ngTransclude) {
168-
// If the attribute is of the form: `ng-transclude="ng-transclude"`
169-
// then treat it like the default
170-
$attrs.ngTransclude = '';
171-
}
166+
compile: function ngTranscludeCompile(tElement) {
167+
168+
// Remove and cache any original content to act as a fallback
169+
var fallbackLinkFn = $compile(tElement.contents());
170+
tElement.empty();
171+
172+
return function ngTranscludePostLink($scope, $element, $attrs, controller, $transclude) {
173+
174+
if (!$transclude) {
175+
throw ngTranscludeMinErr('orphan',
176+
'Illegal use of ngTransclude directive in the template! ' +
177+
'No parent directive that requires a transclusion found. ' +
178+
'Element: {0}',
179+
startingTag($element));
180+
}
172181

173-
function ngTranscludeCloneAttachFn(clone, transcludedScope) {
174-
if (clone.length) {
175-
$element.empty();
176-
$element.append(clone);
177-
} else {
178-
// Since this is the fallback content rather than the transcluded content,
179-
// we compile against the scope we were linked against rather than the transcluded
180-
// scope since this is the directive's own content
181-
$compile($element.contents())($scope);
182182

183-
// There is nothing linked against the transcluded scope since no content was available,
184-
// so it should be safe to clean up the generated scope.
185-
transcludedScope.$destroy();
183+
// If the attribute is of the form: `ng-transclude="ng-transclude"` then treat it like the default
184+
if ($attrs.ngTransclude === $attrs.$attr.ngTransclude) {
185+
$attrs.ngTransclude = '';
186186
}
187-
}
187+
var slotName = $attrs.ngTransclude || $attrs.ngTranscludeSlot;
188188

189-
if (!$transclude) {
190-
throw ngTranscludeMinErr('orphan',
191-
'Illegal use of ngTransclude directive in the template! ' +
192-
'No parent directive that requires a transclusion found. ' +
193-
'Element: {0}',
194-
startingTag($element));
195-
}
189+
// If the slot is required and no transclusion content is provided then this call will throw an error
190+
$transclude(ngTranscludeCloneAttachFn, null, slotName);
196191

197-
// If there is no slot name defined or the slot name is not optional
198-
// then transclude the slot
199-
var slotName = $attrs.ngTransclude || $attrs.ngTranscludeSlot;
200-
$transclude(ngTranscludeCloneAttachFn, null, slotName);
192+
// If the slot is optional and no transclusion content is provided then use the fallback content
193+
if (slotName && !$transclude.isSlotFilled(slotName)) {
194+
useFallbackContent();
195+
}
196+
197+
function ngTranscludeCloneAttachFn(clone, transcludedScope) {
198+
if (clone.length) {
199+
$element.append(clone);
200+
} else {
201+
useFallbackContent();
202+
// There is nothing linked against the transcluded scope since no content was available,
203+
// so it should be safe to clean up the generated scope.
204+
transcludedScope.$destroy();
205+
}
206+
}
207+
208+
function useFallbackContent() {
209+
// Since this is the fallback content rather than the transcluded content,
210+
// we link against the scope of this directive rather than the transcluded scope
211+
fallbackLinkFn($scope, function(clone) {
212+
$element.append(clone);
213+
});
214+
}
215+
};
201216
}
202217
};
203218
}];

test/ng/compileSpec.js

Lines changed: 87 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -7918,17 +7918,52 @@ describe('$compile', function() {
79187918
});
79197919

79207920

7921-
it('should not compile the fallback content if transcluded content is provided', function() {
7922-
var contentsDidLink = false;
7921+
it('should clear the fallback content from the element during compile and before linking', function() {
7922+
module(function() {
7923+
directive('trans', function() {
7924+
return {
7925+
transclude: true,
7926+
template: '<div ng-transclude>fallback content</div>'
7927+
};
7928+
});
7929+
});
7930+
inject(function(log, $rootScope, $compile) {
7931+
element = jqLite('<div trans></div>');
7932+
var linkfn = $compile(element);
7933+
expect(element.html()).toEqual('<div ng-transclude=""></div>');
7934+
linkfn($rootScope);
7935+
$rootScope.$apply();
7936+
expect(sortedHtml(element.html())).toEqual('<div ng-transclude=""><span>fallback content</span></div>');
7937+
});
7938+
});
7939+
7940+
7941+
it('should allow cloning of the fallback via ngRepeat', function() {
7942+
module(function() {
7943+
directive('trans', function() {
7944+
return {
7945+
transclude: true,
7946+
template: '<div ng-repeat="i in [0,1,2]"><div ng-transclude>{{i}}</div></div>'
7947+
};
7948+
});
7949+
});
7950+
inject(function(log, $rootScope, $compile) {
7951+
element = $compile('<div trans></div>')($rootScope);
7952+
$rootScope.$apply();
7953+
expect(element.text()).toEqual('012');
7954+
});
7955+
});
7956+
7957+
7958+
it('should not link the fallback content if transcluded content is provided', function() {
7959+
var linkSpy = jasmine.createSpy('postlink');
79237960

79247961
module(function() {
79257962
directive('inner', function() {
79267963
return {
79277964
restrict: 'E',
79287965
template: 'old stuff! ',
7929-
link: function() {
7930-
contentsDidLink = true;
7931-
}
7966+
link: linkSpy
79327967
};
79337968
});
79347969

@@ -7943,21 +7978,19 @@ describe('$compile', function() {
79437978
element = $compile('<div trans>unicorn!</div>')($rootScope);
79447979
$rootScope.$apply();
79457980
expect(sortedHtml(element.html())).toEqual('<div ng-transclude=""><span>unicorn!</span></div>');
7946-
expect(contentsDidLink).toBe(false);
7981+
expect(linkSpy).not.toHaveBeenCalled();
79477982
});
79487983
});
79497984

79507985
it('should compile and link the fallback content if no transcluded content is provided', function() {
7951-
var contentsDidLink = false;
7986+
var linkSpy = jasmine.createSpy('postlink');
79527987

79537988
module(function() {
79547989
directive('inner', function() {
79557990
return {
79567991
restrict: 'E',
79577992
template: 'old stuff! ',
7958-
link: function() {
7959-
contentsDidLink = true;
7960-
}
7993+
link: linkSpy
79617994
};
79627995
});
79637996

@@ -7972,7 +8005,50 @@ describe('$compile', function() {
79728005
element = $compile('<div trans></div>')($rootScope);
79738006
$rootScope.$apply();
79748007
expect(sortedHtml(element.html())).toEqual('<div ng-transclude=""><inner>old stuff! </inner></div>');
7975-
expect(contentsDidLink).toBe(true);
8008+
expect(linkSpy).toHaveBeenCalled();
8009+
});
8010+
});
8011+
8012+
it('should compile and link the fallback content if an optional transclusion slot is not provided', function() {
8013+
var linkSpy = jasmine.createSpy('postlink');
8014+
8015+
module(function() {
8016+
directive('inner', function() {
8017+
return {
8018+
restrict: 'E',
8019+
template: 'old stuff! ',
8020+
link: linkSpy
8021+
};
8022+
});
8023+
8024+
directive('trans', function() {
8025+
return {
8026+
transclude: { optionalSlot: '?optional'},
8027+
template: '<div ng-transclude="optionalSlot"><inner></inner></div>'
8028+
};
8029+
});
8030+
});
8031+
inject(function(log, $rootScope, $compile) {
8032+
element = $compile('<div trans></div>')($rootScope);
8033+
$rootScope.$apply();
8034+
expect(sortedHtml(element.html())).toEqual('<div ng-transclude="optionalSlot"><inner>old stuff! </inner></div>');
8035+
expect(linkSpy).toHaveBeenCalled();
8036+
});
8037+
});
8038+
8039+
it('should cope if there is neither transcluded content nor fallback content', function() {
8040+
module(function() {
8041+
directive('trans', function() {
8042+
return {
8043+
transclude: true,
8044+
template: '<div ng-transclude></div>'
8045+
};
8046+
});
8047+
});
8048+
inject(function($rootScope, $compile) {
8049+
element = $compile('<div trans></div>')($rootScope);
8050+
$rootScope.$apply();
8051+
expect(sortedHtml(element.html())).toEqual('<div ng-transclude=""></div>');
79768052
});
79778053
});
79788054

@@ -9774,37 +9850,6 @@ describe('$compile', function() {
97749850
expect(element.children().eq(2).text()).toEqual('dorothy');
97759851
});
97769852
});
9777-
9778-
it('should not overwrite the contents of an `ng-transclude` element, if the matching optional slot is not filled', function() {
9779-
module(function() {
9780-
directive('minionComponent', function() {
9781-
return {
9782-
restrict: 'E',
9783-
scope: {},
9784-
transclude: {
9785-
minionSlot: 'minion',
9786-
bossSlot: '?boss'
9787-
},
9788-
template:
9789-
'<div class="boss" ng-transclude="bossSlot">default boss content</div>' +
9790-
'<div class="minion" ng-transclude="minionSlot">default minion content</div>' +
9791-
'<div class="other" ng-transclude>default content</div>'
9792-
};
9793-
});
9794-
});
9795-
inject(function($rootScope, $compile) {
9796-
element = $compile(
9797-
'<minion-component>' +
9798-
'<minion>stuart</minion>' +
9799-
'<span>dorothy</span>' +
9800-
'<minion>kevin</minion>' +
9801-
'</minion-component>')($rootScope);
9802-
$rootScope.$apply();
9803-
expect(element.children().eq(0).text()).toEqual('default boss content');
9804-
expect(element.children().eq(1).text()).toEqual('stuartkevin');
9805-
expect(element.children().eq(2).text()).toEqual('dorothy');
9806-
});
9807-
});
98089853
});
98099854

98109855

0 commit comments

Comments
 (0)