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

fix(ngTransclude): correctly support fallback content #14775

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 37 additions & 27 deletions src/ng/directive/ngTransclude.js
Original file line number Diff line number Diff line change
Expand Up @@ -159,35 +159,45 @@
* </example>
*/
var ngTranscludeMinErr = minErr('ngTransclude');
var ngTranscludeDirective = ngDirective({
restrict: 'EAC',
link: function($scope, $element, $attrs, controller, $transclude) {
var ngTranscludeDirective = ['$compile', function($compile) {
return {
restrict: 'EAC',
terminal: true,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does the fact that we will compile the fallback content later (if necessary) introduce a timing difference with the previous implementation? I.e. is the $transclude function called synchronously or will the uncompiled content stay visible for longer?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, so we could add a compile function that extracts the contents and removes if from the document so that it is hidden straight away

Copy link
Contributor Author

@dcherman dcherman Jun 15, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since linking is a sync process (at least as far as ngTransclude is concerned), I'm pretty sure you shouldn't be able to observe the uncompiled content.

The timing of the pre and post linking functions may have changed in relation to to cousin or aunt/uncle directives (yay DOM family!), however I believe that the timing should remain unchanged in relation to ancestor directives which is the more important part.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm mainly concerned about the timing of rendering (so users won't see flashing {{ ... }}s).
From what I understand from @dcherman's comment (because I was too lazy to read the source code 😛), this hasn't changed. So we're good 👍

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There was a problem in this PR with failing to link fallback content if there was a no content provided for an optional named slot. My #14787 fixes that

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 = '';
}

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 = '';
}
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);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should probably check to see if any contents exist prior to calling $compile.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not semantically necessary as the compile bails out if there is nothing to compile.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

although it could improve performance slightly


function ngTranscludeCloneAttachFn(clone) {
if (clone.length) {
$element.empty();
$element.append(clone);
// 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();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, this is essentially fixing a leak, right ? Cool

}
}
}

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 (!$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);
}
};
}];
32 changes: 29 additions & 3 deletions test/ng/compileSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -7937,35 +7937,61 @@ describe('$compile', function() {

it('should clear contents of the ng-translude element before appending transcluded content' +
' if transcluded content exists', function() {
var contentsDidLink = false;

module(function() {
directive('inner', function() {
return {
restrict: 'E',
template: 'old stuff! ',
link: function() {
contentsDidLink = true;
}
};
});

directive('trans', function() {
return {
transclude: true,
template: '<div ng-transclude>old stuff! </div>'
template: '<div ng-transclude><inner></inner></div>'
};
});
});
inject(function($rootScope, $compile) {
element = $compile('<div trans>unicorn!</div>')($rootScope);
$rootScope.$apply();
expect(sortedHtml(element.html())).toEqual('<div ng-transclude="">unicorn!</div>');
expect(contentsDidLink).toBe(false);
});
});

it('should NOT clear contents of the ng-translude element before appending transcluded content' +
' if transcluded content does NOT exist', function() {
var contentsDidLink = false;

module(function() {
directive('inner', function() {
return {
restrict: 'E',
template: 'old stuff! ',
link: function() {
contentsDidLink = true;
}
};
});

directive('trans', function() {
return {
transclude: true,
template: '<div ng-transclude>old stuff! </div>'
template: '<div ng-transclude><inner></inner></div>'
};
});
});
inject(function(log, $rootScope, $compile) {
element = $compile('<div trans></div>')($rootScope);
$rootScope.$apply();
expect(sortedHtml(element.html())).toEqual('<div ng-transclude="">old stuff! </div>');
expect(sortedHtml(element.html())).toEqual('<div ng-transclude=""><inner>old stuff! </inner></div>');
expect(contentsDidLink).toBe(true);
});
});

Expand Down