fix(ngTransclude): ensure that fallback content is compiled and linked correctly

Closes #14787
This commit is contained in:
Peter Bacon Darwin
2016-06-16 13:46:58 +01:00
parent 0ba14e1853
commit 41f3269bfb
2 changed files with 132 additions and 72 deletions
+45 -30
View File
@@ -163,41 +163,56 @@ var ngTranscludeDirective = ['$compile', function($compile) {
return {
restrict: 'EAC',
terminal: true,
link: function($scope, $element, $attrs, controller, $transclude) {
if ($attrs.ngTransclude === $attrs.$attr.ngTransclude) {
// If the attribute is of the form: `ng-transclude="ng-transclude"`
// then treat it like the default
$attrs.ngTransclude = '';
}
compile: function ngTranscludeCompile(tElement) {
function ngTranscludeCloneAttachFn(clone, transcludedScope) {
if (clone.length) {
$element.empty();
$element.append(clone);
} else {
// Since this is the fallback content rather than the transcluded content,
// we compile against the scope we were linked against rather than the transcluded
// scope since this is the directive's own content
$compile($element.contents())($scope);
// Remove and cache any original content to act as a fallback
var fallbackLinkFn = $compile(tElement.contents());
tElement.empty();
// There is nothing linked against the transcluded scope since no content was available,
// so it should be safe to clean up the generated scope.
transcludedScope.$destroy();
return function ngTranscludePostLink($scope, $element, $attrs, controller, $transclude) {
if (!$transclude) {
throw ngTranscludeMinErr('orphan',
'Illegal use of ngTransclude directive in the template! ' +
'No parent directive that requires a transclusion found. ' +
'Element: {0}',
startingTag($element));
}
}
if (!$transclude) {
throw ngTranscludeMinErr('orphan',
'Illegal use of ngTransclude directive in the template! ' +
'No parent directive that requires a transclusion found. ' +
'Element: {0}',
startingTag($element));
}
// If there is no slot name defined or the slot name is not optional
// then transclude the slot
var slotName = $attrs.ngTransclude || $attrs.ngTranscludeSlot;
$transclude(ngTranscludeCloneAttachFn, null, slotName);
// If the attribute is of the form: `ng-transclude="ng-transclude"` then treat it like the default
if ($attrs.ngTransclude === $attrs.$attr.ngTransclude) {
$attrs.ngTransclude = '';
}
var slotName = $attrs.ngTransclude || $attrs.ngTranscludeSlot;
// If the slot is required and no transclusion content is provided then this call will throw an error
$transclude(ngTranscludeCloneAttachFn, null, slotName);
// If the slot is optional and no transclusion content is provided then use the fallback content
if (slotName && !$transclude.isSlotFilled(slotName)) {
useFallbackContent();
}
function ngTranscludeCloneAttachFn(clone, transcludedScope) {
if (clone.length) {
$element.append(clone);
} else {
useFallbackContent();
// There is nothing linked against the transcluded scope since no content was available,
// so it should be safe to clean up the generated scope.
transcludedScope.$destroy();
}
}
function useFallbackContent() {
// Since this is the fallback content rather than the transcluded content,
// we link against the scope of this directive rather than the transcluded scope
fallbackLinkFn($scope, function(clone) {
$element.append(clone);
});
}
};
}
};
}];
+87 -42
View File
@@ -7970,17 +7970,52 @@ describe('$compile', function() {
});
it('should not compile the fallback content if transcluded content is provided', function() {
var contentsDidLink = false;
it('should clear the fallback content from the element during compile and before linking', function() {
module(function() {
directive('trans', function() {
return {
transclude: true,
template: '<div ng-transclude>fallback content</div>'
};
});
});
inject(function(log, $rootScope, $compile) {
element = jqLite('<div trans></div>');
var linkfn = $compile(element);
expect(element.html()).toEqual('<div ng-transclude=""></div>');
linkfn($rootScope);
$rootScope.$apply();
expect(sortedHtml(element.html())).toEqual('<div ng-transclude="">fallback content</div>');
});
});
it('should allow cloning of the fallback via ngRepeat', function() {
module(function() {
directive('trans', function() {
return {
transclude: true,
template: '<div ng-repeat="i in [0,1,2]"><div ng-transclude>{{i}}</div></div>'
};
});
});
inject(function(log, $rootScope, $compile) {
element = $compile('<div trans></div>')($rootScope);
$rootScope.$apply();
expect(element.text()).toEqual('012');
});
});
it('should not link the fallback content if transcluded content is provided', function() {
var linkSpy = jasmine.createSpy('postlink');
module(function() {
directive('inner', function() {
return {
restrict: 'E',
template: 'old stuff! ',
link: function() {
contentsDidLink = true;
}
link: linkSpy
};
});
@@ -7995,21 +8030,19 @@ describe('$compile', function() {
element = $compile('<div trans>unicorn!</div>')($rootScope);
$rootScope.$apply();
expect(sortedHtml(element.html())).toEqual('<div ng-transclude="">unicorn!</div>');
expect(contentsDidLink).toBe(false);
expect(linkSpy).not.toHaveBeenCalled();
});
});
it('should compile and link the fallback content if no transcluded content is provided', function() {
var contentsDidLink = false;
var linkSpy = jasmine.createSpy('postlink');
module(function() {
directive('inner', function() {
return {
restrict: 'E',
template: 'old stuff! ',
link: function() {
contentsDidLink = true;
}
link: linkSpy
};
});
@@ -8024,7 +8057,50 @@ describe('$compile', function() {
element = $compile('<div trans></div>')($rootScope);
$rootScope.$apply();
expect(sortedHtml(element.html())).toEqual('<div ng-transclude=""><inner>old stuff! </inner></div>');
expect(contentsDidLink).toBe(true);
expect(linkSpy).toHaveBeenCalled();
});
});
it('should compile and link the fallback content if an optional transclusion slot is not provided', function() {
var linkSpy = jasmine.createSpy('postlink');
module(function() {
directive('inner', function() {
return {
restrict: 'E',
template: 'old stuff! ',
link: linkSpy
};
});
directive('trans', function() {
return {
transclude: { optionalSlot: '?optional'},
template: '<div ng-transclude="optionalSlot"><inner></inner></div>'
};
});
});
inject(function(log, $rootScope, $compile) {
element = $compile('<div trans></div>')($rootScope);
$rootScope.$apply();
expect(sortedHtml(element.html())).toEqual('<div ng-transclude="optionalSlot"><inner>old stuff! </inner></div>');
expect(linkSpy).toHaveBeenCalled();
});
});
it('should cope if there is neither transcluded content nor fallback content', function() {
module(function() {
directive('trans', function() {
return {
transclude: true,
template: '<div ng-transclude></div>'
};
});
});
inject(function($rootScope, $compile) {
element = $compile('<div trans></div>')($rootScope);
$rootScope.$apply();
expect(sortedHtml(element.html())).toEqual('<div ng-transclude=""></div>');
});
});
@@ -9824,37 +9900,6 @@ describe('$compile', function() {
expect(element.children().eq(2).text()).toEqual('dorothy');
});
});
it('should not overwrite the contents of an `ng-transclude` element, if the matching optional slot is not filled', function() {
module(function() {
directive('minionComponent', function() {
return {
restrict: 'E',
scope: {},
transclude: {
minionSlot: 'minion',
bossSlot: '?boss'
},
template:
'<div class="boss" ng-transclude="bossSlot">default boss content</div>' +
'<div class="minion" ng-transclude="minionSlot">default minion content</div>' +
'<div class="other" ng-transclude>default content</div>'
};
});
});
inject(function($rootScope, $compile) {
element = $compile(
'<minion-component>' +
'<minion>stuart</minion>' +
'<span>dorothy</span>' +
'<minion>kevin</minion>' +
'</minion-component>')($rootScope);
$rootScope.$apply();
expect(element.children().eq(0).text()).toEqual('default boss content');
expect(element.children().eq(1).text()).toEqual('stuartkevin');
expect(element.children().eq(2).text()).toEqual('dorothy');
});
});
});