Skip to content
This repository was archived by the owner on Apr 12, 2024. It is now read-only.
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 649a7cd

Browse files
committedOct 1, 2015
feat($compile): multiple transclusion via named slots
1 parent 049d3de commit 649a7cd

File tree

3 files changed

+423
-7
lines changed

3 files changed

+423
-7
lines changed
 

‎src/ng/compile.js

+53
Original file line numberDiff line numberDiff line change
@@ -1486,6 +1486,13 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
14861486
});
14871487
};
14881488

1489+
// We need to attach the transclusion slots onto the boundTranscludeFn
1490+
// so that they are available for directives such as `ngTransclude`
1491+
var boundSlots = boundTranscludeFn.$$slots = createMap();
1492+
for (var slotName in transcludeFn.$$slots) {
1493+
boundSlots[slotName] = createBoundTranscludeFn(scope, transcludeFn.$$slots[slotName], previousBoundTranscludeFn);
1494+
}
1495+
14891496
return boundTranscludeFn;
14901497
}
14911498

@@ -1826,6 +1833,52 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
18261833
// on the same element more than once.
18271834
nonTlbTranscludeDirective: nonTlbTranscludeDirective
18281835
});
1836+
} else if (isObject(directiveValue)) {
1837+
1838+
// We have multiple transclusion zones - match them and collect them up into slots
1839+
$template = [];
1840+
var slots = createMap();
1841+
var slotNames = createMap();
1842+
var filledSlots = createMap();
1843+
1844+
// Parse the slot names: if they start with a ? then they are optional
1845+
forEach(directiveValue, function(slotName, key) {
1846+
var optional = (slotName.charAt(0) === '?');
1847+
slotName = optional ? slotName.substring(1) : slotName;
1848+
slotNames[key] = slotName;
1849+
slots[slotName] = [];
1850+
// filledSlots will contain true for all slots that are either
1851+
// optional or have been filled
1852+
filledSlots[slotName] = optional;
1853+
});
1854+
1855+
1856+
forEach($compileNode.children(), function(node) {
1857+
var slotName = slotNames[directiveNormalize(nodeName_(node))];
1858+
var slot = $template;
1859+
if (slotName) {
1860+
filledSlots[slotName] = true;
1861+
slots[slotName].push(node);
1862+
} else {
1863+
$template.push(node);
1864+
}
1865+
});
1866+
1867+
// Check for required slots that were not filled
1868+
forEach(filledSlots, function(filled, slotName) {
1869+
if (!filled) {
1870+
throw $compileMinErr('reqslot', 'Required transclusion slot `{0}` was not filled.', slotName);
1871+
}
1872+
});
1873+
1874+
// Compile each slot into a transclusion function and attach them to the default transclusion function
1875+
childTranscludeFn = compilationGenerator(mightHaveMultipleTransclusionError, $template, transcludeFn);
1876+
forEach(Object.keys(slots), function(slotName) {
1877+
slots[slotName] = compilationGenerator(mightHaveMultipleTransclusionError, slots[slotName], transcludeFn);
1878+
});
1879+
childTranscludeFn.$$slots = slots;
1880+
$compileNode.empty(); // clear contents
1881+
18291882
} else {
18301883
$template = jqLite(jqLiteClone(compileNode)).contents();
18311884
$compileNode.empty(); // clear contents

‎src/ng/directive/ngTransclude.js

+80-7
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,20 @@
88
* @description
99
* Directive that marks the insertion point for the transcluded DOM of the nearest parent directive that uses transclusion.
1010
*
11+
* You can specify that you want to insert a named transclusion slot, instead of the default slot, by providing the slot name
12+
* as the value of the `ng-transclude` or `ng-transclude-slot` attribute.
13+
*
1114
* Any existing content of the element that this directive is placed on will be removed before the transcluded content is inserted.
1215
*
1316
* @element ANY
1417
*
18+
* @param {string} ngTransclude|ngTranscludeSlot the name of the slot to insert at this point. If this is not provided or empty then
19+
* the default slot is used.
20+
*
1521
* @example
16-
<example module="transcludeExample">
22+
* ### Default transclusion
23+
* This example demonstrates simple transclusion.
24+
<example name="simpleTranscludeExample" module="transcludeExample">
1725
<file name="index.html">
1826
<script>
1927
angular.module('transcludeExample', [])
@@ -53,21 +61,86 @@
5361
</file>
5462
</example>
5563
*
56-
*/
64+
* @example
65+
* ### Multi-slot transclusion
66+
<example name="multiSlotTranscludeExample" module="multiSlotTranscludeExample">
67+
<file name="index.html">
68+
<div ng-controller="ExampleController">
69+
<input ng-model="title" aria-label="title"> <br/>
70+
<textarea ng-model="text" aria-label="text"></textarea> <br/>
71+
<pane>
72+
<pane-title><a ng-href="{{link}}">{{title}}</a></pane-title>
73+
<pane-body><p>{{text}}</p></pane-body>
74+
</pane>
75+
</div>
76+
</file>
77+
<file name="app.js">
78+
angular.module('multiSlotTranscludeExample', [])
79+
.directive('pane', function(){
80+
return {
81+
restrict: 'E',
82+
transclude: {
83+
'paneTitle': '?title',
84+
'paneBody': 'body'
85+
},
86+
template: '<div style="border: 1px solid black;">' +
87+
'<div ng-transclude="title" style="background-color: gray"></div>' +
88+
'<div ng-transclude="body"></div>' +
89+
'</div>'
90+
};
91+
})
92+
.controller('ExampleController', ['$scope', function($scope) {
93+
$scope.title = 'Lorem Ipsum';
94+
$scope.link = "https://google.com";
95+
$scope.text = 'Neque porro quisquam est qui dolorem ipsum quia dolor...';
96+
}]);
97+
</file>
98+
<file name="protractor.js" type="protractor">
99+
it('should have transcluded the title and the body', function() {
100+
var titleElement = element(by.model('title'));
101+
titleElement.clear();
102+
titleElement.sendKeys('TITLE');
103+
var textElement = element(by.model('text'));
104+
textElement.clear();
105+
textElement.sendKeys('TEXT');
106+
expect(element(by.binding('title')).getText()).toEqual('TITLE');
107+
expect(element(by.binding('text')).getText()).toEqual('TEXT');
108+
});
109+
</file>
110+
</example> */
111+
var ngTranscludeMinErr = minErr('ngTransclude');
57112
var ngTranscludeDirective = ngDirective({
58113
restrict: 'EAC',
59114
link: function($scope, $element, $attrs, controller, $transclude) {
115+
116+
function ngTranscludeCloneAttachFn(clone) {
117+
$element.empty();
118+
$element.append(clone);
119+
}
120+
60121
if (!$transclude) {
61-
throw minErr('ngTransclude')('orphan',
122+
throw ngTranscludeMinErr('orphan',
62123
'Illegal use of ngTransclude directive in the template! ' +
63124
'No parent directive that requires a transclusion found. ' +
64125
'Element: {0}',
65126
startingTag($element));
66127
}
67128

68-
$transclude(function(clone) {
69-
$element.empty();
70-
$element.append(clone);
71-
});
129+
var slotName = $attrs.ngTransclude || $attrs.ngTranscludeSlot;
130+
if (slotName) {
131+
$transclude = $transclude.$$boundTransclude.$$slots[slotName];
132+
if ($transclude) {
133+
$transclude(undefined, ngTranscludeCloneAttachFn);
134+
} else {
135+
throw ngTranscludeMinErr('orphan',
136+
'Illegal use of ngTransclude directive in the template! ' +
137+
'No parent directive that requires a transclusion with slot name "{0}". ' +
138+
'Element: {1}',
139+
slotName, startingTag($element));
140+
}
141+
} else {
142+
$transclude(ngTranscludeCloneAttachFn);
143+
}
72144
}
73145
});
146+

‎test/ng/compileSpec.js

+290
Original file line numberDiff line numberDiff line change
@@ -7265,6 +7265,296 @@ describe('$compile', function() {
72657265
});
72667266

72677267

7268+
describe('multi-slot transclude', function() {
7269+
it('should only include elements without a matching transclusion element in default transclusion slot', function() {
7270+
module(function() {
7271+
directive('minionComponent', function() {
7272+
return {
7273+
restrict: 'E',
7274+
scope: {},
7275+
transclude: {
7276+
boss: 'bossSlot'
7277+
},
7278+
template:
7279+
'<div class="other" ng-transclude></div>'
7280+
};
7281+
});
7282+
});
7283+
inject(function($rootScope, $compile) {
7284+
element = $compile(
7285+
'<minion-component>' +
7286+
'<span>stuart</span>' +
7287+
'<span>bob</span>' +
7288+
'<boss>gru</boss>' +
7289+
'<span>kevin</span>' +
7290+
'</minion-component>')($rootScope);
7291+
$rootScope.$apply();
7292+
expect(element.text()).toEqual('stuartbobkevin');
7293+
});
7294+
});
7295+
7296+
it('should transclude elements to an `ng-transclude` with a matching transclusion slot name', function() {
7297+
module(function() {
7298+
directive('minionComponent', function() {
7299+
return {
7300+
restrict: 'E',
7301+
scope: {},
7302+
transclude: {
7303+
minion: 'minionSlot',
7304+
boss: 'bossSlot'
7305+
},
7306+
template:
7307+
'<div class="boss" ng-transclude="bossSlot"></div>' +
7308+
'<div class="minion" ng-transclude="minionSlot"></div>' +
7309+
'<div class="other" ng-transclude></div>'
7310+
};
7311+
});
7312+
});
7313+
inject(function($rootScope, $compile) {
7314+
element = $compile(
7315+
'<minion-component>' +
7316+
'<minion>stuart</minion>' +
7317+
'<span>dorothy</span>' +
7318+
'<boss>gru</boss>' +
7319+
'<minion>kevin</minion>' +
7320+
'</minion-component>')($rootScope);
7321+
$rootScope.$apply();
7322+
expect(element.children().eq(0).text()).toEqual('gru');
7323+
expect(element.children().eq(1).text()).toEqual('stuartkevin');
7324+
expect(element.children().eq(2).text()).toEqual('dorothy');
7325+
});
7326+
});
7327+
7328+
7329+
it('should use the `ng-transclude-slot` attribute if ng-transclude is used as an element', function() {
7330+
module(function() {
7331+
directive('minionComponent', function() {
7332+
return {
7333+
restrict: 'E',
7334+
scope: {},
7335+
transclude: {
7336+
minion: 'minionSlot',
7337+
boss: 'bossSlot'
7338+
},
7339+
template:
7340+
'<ng-transclude class="boss" ng-transclude-slot="bossSlot"></ng-transclude>' +
7341+
'<ng-transclude class="minion" ng-transclude-slot="minionSlot"></ng-transclude>' +
7342+
'<ng-transclude class="other"></ng-transclude>'
7343+
};
7344+
});
7345+
});
7346+
inject(function($rootScope, $compile) {
7347+
element = $compile(
7348+
'<minion-component>' +
7349+
'<minion>stuart</minion>' +
7350+
'<span>dorothy</span>' +
7351+
'<boss>gru</boss>' +
7352+
'<minion>kevin</minion>' +
7353+
'</minion-component>')($rootScope);
7354+
$rootScope.$apply();
7355+
expect(element.children().eq(0).text()).toEqual('gru');
7356+
expect(element.children().eq(1).text()).toEqual('stuartkevin');
7357+
expect(element.children().eq(2).text()).toEqual('dorothy');
7358+
});
7359+
});
7360+
7361+
it('should error if a required transclude slot is not filled', function() {
7362+
module(function() {
7363+
directive('minionComponent', function() {
7364+
return {
7365+
restrict: 'E',
7366+
scope: {},
7367+
transclude: {
7368+
minion: 'minionSlot',
7369+
boss: 'bossSlot'
7370+
},
7371+
template:
7372+
'<div class="boss" ng-transclude="bossSlot"></div>' +
7373+
'<div class="minion" ng-transclude="minionSlot"></div>' +
7374+
'<div class="other" ng-transclude></div>'
7375+
};
7376+
});
7377+
});
7378+
inject(function($rootScope, $compile) {
7379+
expect(function() {
7380+
element = $compile(
7381+
'<minion-component>' +
7382+
'<minion>stuart</minion>' +
7383+
'<span>dorothy</span>' +
7384+
'</minion-component>')($rootScope);
7385+
}).toThrowMinErr('$compile', 'reqslot', 'Required transclusion slot `bossSlot` was not filled.');
7386+
});
7387+
});
7388+
7389+
7390+
it('should not error if an optional transclude slot is not filled', function() {
7391+
module(function() {
7392+
directive('minionComponent', function() {
7393+
return {
7394+
restrict: 'E',
7395+
scope: {},
7396+
transclude: {
7397+
minion: 'minionSlot',
7398+
boss: '?bossSlot'
7399+
},
7400+
template:
7401+
'<div class="boss" ng-transclude="bossSlot"></div>' +
7402+
'<div class="minion" ng-transclude="minionSlot"></div>' +
7403+
'<div class="other" ng-transclude></div>'
7404+
};
7405+
});
7406+
});
7407+
inject(function($rootScope, $compile) {
7408+
element = $compile(
7409+
'<minion-component>' +
7410+
'<minion>stuart</minion>' +
7411+
'<span>dorothy</span>' +
7412+
'</minion-component>')($rootScope);
7413+
$rootScope.$apply();
7414+
expect(element.children().eq(1).text()).toEqual('stuart');
7415+
expect(element.children().eq(2).text()).toEqual('dorothy');
7416+
});
7417+
});
7418+
7419+
7420+
it('should error if we try to transclude a slot that was not declared by the directive', function() {
7421+
module(function() {
7422+
directive('minionComponent', function() {
7423+
return {
7424+
restrict: 'E',
7425+
scope: {},
7426+
transclude: {
7427+
minion: 'minionSlot'
7428+
},
7429+
template:
7430+
'<div class="boss" ng-transclude="bossSlot"></div>' +
7431+
'<div class="minion" ng-transclude="minionSlot"></div>' +
7432+
'<div class="other" ng-transclude></div>'
7433+
};
7434+
});
7435+
});
7436+
inject(function($rootScope, $compile) {
7437+
expect(function() {
7438+
element = $compile(
7439+
'<minion-component>' +
7440+
'<minion>stuart</minion>' +
7441+
'<span>dorothy</span>' +
7442+
'</minion-component>')($rootScope);
7443+
}).toThrowMinErr('ngTransclude', 'orphan',
7444+
'Illegal use of ngTransclude directive in the template! ' +
7445+
'No parent directive that requires a transclusion with slot name "bossSlot". ' +
7446+
'Element: <div class="boss" ng-transclude="bossSlot">');
7447+
});
7448+
});
7449+
7450+
it('should allow the slot name to equal the element name', function() {
7451+
7452+
module(function() {
7453+
directive('foo', function() {
7454+
return {
7455+
restrict: 'E',
7456+
scope: {},
7457+
transclude: {
7458+
bar: 'bar'
7459+
},
7460+
template:
7461+
'<div class="other" ng-transclude="bar"></div>'
7462+
};
7463+
});
7464+
});
7465+
inject(function($rootScope, $compile) {
7466+
element = $compile(
7467+
'<foo>' +
7468+
'<bar>baz</bar>' +
7469+
'</foo>')($rootScope);
7470+
$rootScope.$apply();
7471+
expect(element.text()).toEqual('baz');
7472+
});
7473+
});
7474+
7475+
7476+
it('should match against the normalized form of the element', function() {
7477+
module(function() {
7478+
directive('foo', function() {
7479+
return {
7480+
restrict: 'E',
7481+
scope: {},
7482+
transclude: {
7483+
fooBar: 'fooBarSlot'
7484+
},
7485+
template:
7486+
'<div class="other" ng-transclude="fooBarSlot"></div>'
7487+
};
7488+
});
7489+
});
7490+
inject(function($rootScope, $compile) {
7491+
element = $compile(
7492+
'<foo>' +
7493+
'<foo-bar>baz</foo-bar>' +
7494+
'</foo>')($rootScope);
7495+
$rootScope.$apply();
7496+
expect(element.text()).toEqual('baz');
7497+
});
7498+
});
7499+
7500+
7501+
it('should provide the elements marked with matching transclude elements as additional transclude functions on the $$slots property', function() {
7502+
var capturedTranscludeFn;
7503+
module(function() {
7504+
directive('minionComponent', function() {
7505+
return {
7506+
restrict: 'E',
7507+
scope: {},
7508+
transclude: {
7509+
minion: 'minionSlot',
7510+
boss: 'bossSlot'
7511+
},
7512+
template:
7513+
'<div class="boss" ng-transclude="bossSlot"></div>' +
7514+
'<div class="minion" ng-transclude="minionSlot"></div>' +
7515+
'<div class="other" ng-transclude></div>',
7516+
link: function(s, e, a, c, transcludeFn) {
7517+
capturedTranscludeFn = transcludeFn;
7518+
}
7519+
};
7520+
});
7521+
});
7522+
inject(function($rootScope, $compile, log) {
7523+
element = $compile(
7524+
'<minion-component>' +
7525+
' <minion>stuart</minion>' +
7526+
' <minion>bob</minion>' +
7527+
' <span>dorothy</span>' +
7528+
' <boss>gru</boss>' +
7529+
'</minion-component>')($rootScope);
7530+
$rootScope.$apply();
7531+
7532+
var minionTranscludeFn = capturedTranscludeFn.$$boundTransclude.$$slots['minionSlot'];
7533+
var minions = minionTranscludeFn();
7534+
expect(minions[0].outerHTML).toEqual('<minion class="ng-scope">stuart</minion>');
7535+
expect(minions[1].outerHTML).toEqual('<minion class="ng-scope">bob</minion>');
7536+
7537+
var scope = element.scope();
7538+
7539+
var minionScope = jqLite(minions[0]).scope();
7540+
expect(minionScope.$parent).toBe(scope);
7541+
7542+
var bossTranscludeFn = capturedTranscludeFn.$$boundTransclude.$$slots['bossSlot'];
7543+
var boss = bossTranscludeFn();
7544+
expect(boss[0].outerHTML).toEqual('<boss class="ng-scope">gru</boss>');
7545+
7546+
var bossScope = jqLite(boss[0]).scope();
7547+
expect(bossScope.$parent).toBe(scope);
7548+
7549+
expect(bossScope).not.toBe(minionScope);
7550+
7551+
dealoc(boss);
7552+
dealoc(minions);
7553+
});
7554+
});
7555+
});
7556+
7557+
72687558
describe('img[src] sanitization', function() {
72697559

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

0 commit comments

Comments
 (0)
This repository has been archived.