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

Commit 41f3269

Browse files
fix(ngTransclude): ensure that fallback content is compiled and linked correctly
Closes #14787
1 parent 0ba14e1 commit 41f3269

File tree

2 files changed

+132
-72
lines changed

2 files changed

+132
-72
lines changed

src/ng/directive/ngTransclude.js

+45-30
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

+87-42
Original file line numberDiff line numberDiff line change
@@ -7970,17 +7970,52 @@ describe('$compile', function() {
79707970
});
79717971

79727972

7973-
it('should not compile the fallback content if transcluded content is provided', function() {
7974-
var contentsDidLink = false;
7973+
it('should clear the fallback content from the element during compile and before linking', function() {
7974+
module(function() {
7975+
directive('trans', function() {
7976+
return {
7977+
transclude: true,
7978+
template: '<div ng-transclude>fallback content</div>'
7979+
};
7980+
});
7981+
});
7982+
inject(function(log, $rootScope, $compile) {
7983+
element = jqLite('<div trans></div>');
7984+
var linkfn = $compile(element);
7985+
expect(element.html()).toEqual('<div ng-transclude=""></div>');
7986+
linkfn($rootScope);
7987+
$rootScope.$apply();
7988+
expect(sortedHtml(element.html())).toEqual('<div ng-transclude="">fallback content</div>');
7989+
});
7990+
});
7991+
7992+
7993+
it('should allow cloning of the fallback via ngRepeat', function() {
7994+
module(function() {
7995+
directive('trans', function() {
7996+
return {
7997+
transclude: true,
7998+
template: '<div ng-repeat="i in [0,1,2]"><div ng-transclude>{{i}}</div></div>'
7999+
};
8000+
});
8001+
});
8002+
inject(function(log, $rootScope, $compile) {
8003+
element = $compile('<div trans></div>')($rootScope);
8004+
$rootScope.$apply();
8005+
expect(element.text()).toEqual('012');
8006+
});
8007+
});
8008+
8009+
8010+
it('should not link the fallback content if transcluded content is provided', function() {
8011+
var linkSpy = jasmine.createSpy('postlink');
79758012

79768013
module(function() {
79778014
directive('inner', function() {
79788015
return {
79798016
restrict: 'E',
79808017
template: 'old stuff! ',
7981-
link: function() {
7982-
contentsDidLink = true;
7983-
}
8018+
link: linkSpy
79848019
};
79858020
});
79868021

@@ -7995,21 +8030,19 @@ describe('$compile', function() {
79958030
element = $compile('<div trans>unicorn!</div>')($rootScope);
79968031
$rootScope.$apply();
79978032
expect(sortedHtml(element.html())).toEqual('<div ng-transclude="">unicorn!</div>');
7998-
expect(contentsDidLink).toBe(false);
8033+
expect(linkSpy).not.toHaveBeenCalled();
79998034
});
80008035
});
80018036

80028037
it('should compile and link the fallback content if no transcluded content is provided', function() {
8003-
var contentsDidLink = false;
8038+
var linkSpy = jasmine.createSpy('postlink');
80048039

80058040
module(function() {
80068041
directive('inner', function() {
80078042
return {
80088043
restrict: 'E',
80098044
template: 'old stuff! ',
8010-
link: function() {
8011-
contentsDidLink = true;
8012-
}
8045+
link: linkSpy
80138046
};
80148047
});
80158048

@@ -8024,7 +8057,50 @@ describe('$compile', function() {
80248057
element = $compile('<div trans></div>')($rootScope);
80258058
$rootScope.$apply();
80268059
expect(sortedHtml(element.html())).toEqual('<div ng-transclude=""><inner>old stuff! </inner></div>');
8027-
expect(contentsDidLink).toBe(true);
8060+
expect(linkSpy).toHaveBeenCalled();
8061+
});
8062+
});
8063+
8064+
it('should compile and link the fallback content if an optional transclusion slot is not provided', function() {
8065+
var linkSpy = jasmine.createSpy('postlink');
8066+
8067+
module(function() {
8068+
directive('inner', function() {
8069+
return {
8070+
restrict: 'E',
8071+
template: 'old stuff! ',
8072+
link: linkSpy
8073+
};
8074+
});
8075+
8076+
directive('trans', function() {
8077+
return {
8078+
transclude: { optionalSlot: '?optional'},
8079+
template: '<div ng-transclude="optionalSlot"><inner></inner></div>'
8080+
};
8081+
});
8082+
});
8083+
inject(function(log, $rootScope, $compile) {
8084+
element = $compile('<div trans></div>')($rootScope);
8085+
$rootScope.$apply();
8086+
expect(sortedHtml(element.html())).toEqual('<div ng-transclude="optionalSlot"><inner>old stuff! </inner></div>');
8087+
expect(linkSpy).toHaveBeenCalled();
8088+
});
8089+
});
8090+
8091+
it('should cope if there is neither transcluded content nor fallback content', function() {
8092+
module(function() {
8093+
directive('trans', function() {
8094+
return {
8095+
transclude: true,
8096+
template: '<div ng-transclude></div>'
8097+
};
8098+
});
8099+
});
8100+
inject(function($rootScope, $compile) {
8101+
element = $compile('<div trans></div>')($rootScope);
8102+
$rootScope.$apply();
8103+
expect(sortedHtml(element.html())).toEqual('<div ng-transclude=""></div>');
80288104
});
80298105
});
80308106

@@ -9824,37 +9900,6 @@ describe('$compile', function() {
98249900
expect(element.children().eq(2).text()).toEqual('dorothy');
98259901
});
98269902
});
9827-
9828-
it('should not overwrite the contents of an `ng-transclude` element, if the matching optional slot is not filled', function() {
9829-
module(function() {
9830-
directive('minionComponent', function() {
9831-
return {
9832-
restrict: 'E',
9833-
scope: {},
9834-
transclude: {
9835-
minionSlot: 'minion',
9836-
bossSlot: '?boss'
9837-
},
9838-
template:
9839-
'<div class="boss" ng-transclude="bossSlot">default boss content</div>' +
9840-
'<div class="minion" ng-transclude="minionSlot">default minion content</div>' +
9841-
'<div class="other" ng-transclude>default content</div>'
9842-
};
9843-
});
9844-
});
9845-
inject(function($rootScope, $compile) {
9846-
element = $compile(
9847-
'<minion-component>' +
9848-
'<minion>stuart</minion>' +
9849-
'<span>dorothy</span>' +
9850-
'<minion>kevin</minion>' +
9851-
'</minion-component>')($rootScope);
9852-
$rootScope.$apply();
9853-
expect(element.children().eq(0).text()).toEqual('default boss content');
9854-
expect(element.children().eq(1).text()).toEqual('stuartkevin');
9855-
expect(element.children().eq(2).text()).toEqual('dorothy');
9856-
});
9857-
});
98589903
});
98599904

98609905

0 commit comments

Comments
 (0)