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

WIP - DO NOT MERGE - feat($compile): multiple transclusion via slots #12742

Closed
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
30 changes: 28 additions & 2 deletions src/ng/compile.js
Original file line number Diff line number Diff line change
Expand Up @@ -1486,6 +1486,11 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
});
};

var boundSlots = boundTranscludeFn.$$slots = createMap();
for (var slotName in transcludeFn.$$slots) {
boundSlots[slotName] = createBoundTranscludeFn(scope, transcludeFn.$$slots[slotName], previousBoundTranscludeFn);
}

return boundTranscludeFn;
}

Expand Down Expand Up @@ -1646,6 +1651,27 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
};
}


function compileTemplate(eager, $templateNodes, parentTranscludeFn, terminalPriority, ignoreDirective, previousCompileContext) {
var defaultSlot = [];
var slots = createMap();
forEach($templateNodes, function(node) {
var slotName = jqLite(node).attr('ng-transclude-slot');
Copy link
Contributor

Choose a reason for hiding this comment

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

in prod version we need to normalize this attribute name, maybe using $attrs

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, this is a bit of a problem to do efficiently since at this stage we haven't extracted the attributes, so we would need to iterate over all the attributes or over all the possible variations of ng-transclude-slot.

var slot = defaultSlot;
if (slotName) {
slot = slots[slotName] = (slots[slotName] || []);
}
slot.push(node);
});

var transcludeFn = compilationGenerator(eager, defaultSlot, parentTranscludeFn, terminalPriority, ignoreDirective, previousCompileContext);
forEach(Object.keys(slots), function(slotName) {
slots[slotName] = compilationGenerator(eager, slots[slotName], parentTranscludeFn, terminalPriority, ignoreDirective, previousCompileContext);
});
transcludeFn.$$slots = slots;
return transcludeFn;
}

/**
* A function generator that is used to support both eager and lazy compilation
* linking function.
Expand Down Expand Up @@ -1815,7 +1841,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
compileNode = $compileNode[0];
replaceWith(jqCollection, sliceArgs($template), compileNode);

childTranscludeFn = compilationGenerator(mightHaveMultipleTransclusionError, $template, transcludeFn, terminalPriority,
childTranscludeFn = compileTemplate(mightHaveMultipleTransclusionError, $template, transcludeFn, terminalPriority,
replaceDirective && replaceDirective.name, {
// Don't pass in:
// - controllerDirectives - otherwise we'll create duplicates controllers
Expand All @@ -1829,7 +1855,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
} else {
$template = jqLite(jqLiteClone(compileNode)).contents();
$compileNode.empty(); // clear contents
childTranscludeFn = compilationGenerator(mightHaveMultipleTransclusionError, $template, transcludeFn);
childTranscludeFn = compileTemplate(mightHaveMultipleTransclusionError, $template, transcludeFn);
}
}

Expand Down
19 changes: 15 additions & 4 deletions src/ng/directive/ngTransclude.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,20 @@ var ngTranscludeDirective = ngDirective({
startingTag($element));
}

$transclude(function(clone) {
$element.empty();
$element.append(clone);
});
if ($attrs.ngTransclude) {
$transclude = $transclude.$$boundTransclude.$$slots[$attrs.ngTransclude];
if (!$transclude) return;

$transclude(undefined, function(clone) {
Copy link
Contributor

Choose a reason for hiding this comment

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

why is the first argument undefined?

Copy link
Contributor

Choose a reason for hiding this comment

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

oh, I see your comments in the PR description.

$element.empty();
$element.append(clone);
});
} else {

$transclude(function(clone) {
$element.empty();
$element.append(clone);
});
}
}
});
98 changes: 98 additions & 0 deletions test/ng/compileSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -7262,6 +7262,104 @@ describe('$compile', function() {
});


describe('multi-slot transclude', function() {
it('should only include elements without `ng-transclude-slot` attribute in default transclusion function', function() {
module(function() {
directive('trans', function() {
return {
transclude: true,
template: '<div ng-transclude></div>'
};
});
});
inject(function($rootScope, $compile) {
element = $compile(
'<div trans>' +
'<span>stuart</span>' +
'<span>bob</span>' +
'<span ng-transclude-slot="boss">gru</span>' +
'<span>kevin</span>' +
'</div>')($rootScope);
$rootScope.$apply();
expect(element.text()).toEqual('stuartbobkevin');
});
});

it('should transclude elements to an `ng-transclude` with a matching `ng-transclude-slot`', function() {
module(function() {
directive('trans', function() {
return {
transclude: true,
template:
'<div class="boss" ng-transclude="boss"></div>' +
'<div class="minion" ng-transclude="minion"></div>' +
'<div class="other" ng-transclude></div>'
};
});
});
inject(function($rootScope, $compile) {
element = $compile(
'<div trans>' +
'<span ng-transclude-slot="minion">stuart</span>' +
'<span>dorothy</span>' +
'<span ng-transclude-slot="boss">gru</span>' +
'<span ng-transclude-slot="minion">kevin</span>' +
'</div>')($rootScope);
$rootScope.$apply();
expect(element.children().eq(0).text()).toEqual('gru');
expect(element.children().eq(1).text()).toEqual('stuartkevin');
expect(element.children().eq(2).text()).toEqual('dorothy');
});
});

it('should provide the elements marked with `ng-transclude-slot` as additional transclude functions on the $$slots property', function() {
var capturedTranscludeFn;
module(function() {
directive('trans', function() {
return {
transclude: true,
link: function(scope, element, attrs, controller, transclude) {
capturedTranscludeFn = transclude;
}
};
});
});
inject(function($rootScope, $compile, log) {
element = $compile(
'<div trans>' +
' <span ng-transclude-slot="minion">stuart</span>' +
' <span ng-transclude-slot="minion">bob</span>' +
' <span>dorothy</span>' +
' <span ng-transclude-slot="boss">gru</span>' +
'</div>')($rootScope);
$rootScope.$apply();

var minionTranscludeFn = capturedTranscludeFn.$$boundTransclude.$$slots['minion'];
var minions = minionTranscludeFn();
expect(minions[0].outerHTML).toEqual('<span ng-transclude-slot="minion" class="ng-scope">stuart</span>');
expect(minions[1].outerHTML).toEqual('<span ng-transclude-slot="minion" class="ng-scope">bob</span>');

var scope = element.scope();

var minionScope = jqLite(minions[0]).scope();
expect(minionScope.$parent).toBe(scope);

var bossTranscludeFn = capturedTranscludeFn.$$boundTransclude.$$slots['boss'];
var boss = bossTranscludeFn();
expect(boss[0].outerHTML).toEqual('<span ng-transclude-slot="boss" class="ng-scope">gru</span>');

var bossScope = jqLite(boss[0]).scope();
expect(bossScope.$parent).toBe(scope);

expect(bossScope).not.toBe(minionScope);

dealoc(boss);
dealoc(minions);
});
});
});


describe('img[src] sanitization', function() {

it('should NOT require trusted values for img src', inject(function($rootScope, $compile, $sce) {
Expand Down