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

feat($compile): support dynamic transclusion slots #14227

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
20 changes: 18 additions & 2 deletions src/ng/compile.js
Original file line number Diff line number Diff line change
Expand Up @@ -536,14 +536,16 @@
* Testing Transclusion Directives}.
* </div>
*
* There are three kinds of transclusion depending upon whether you want to transclude just the contents of the
* directive's element, the entire element or multiple parts of the element contents:
* There are four kinds of transclusion depending upon whether you want to transclude just the contents of the
* directive's element, the entire element, multiple parts of the element contents, or multiple parts dynamically:
*
* * `true` - transclude the content (i.e. the child nodes) of the directive's element.
* * `'element'` - transclude the whole of the directive's element including any directives on this
* element that defined at a lower priority than this directive. When used, the `template`
* property is ignored.
* * **`{...}` (an object hash):** - map elements of the content onto transclusion "slots" in the template.
* * `'dynamic'` - allow dynamic mulit-slot transclusion, by providing the slot configuration when using the component
* (in the transclusion-slot attrbiute)
*
* **Mult-slot transclusion** is declared by providing an object for the `transclude` property.
*
Expand All @@ -563,6 +565,13 @@
* `$transclude.isSlotFilled(slotName)` on the transclude function passed to the directive's link function and
* injectable into the directive's controller.
*
* **Dynamic mult-slot transclusion** is declared by using the 'dynamic' value on the component and then passing the
* transclusion slot configuration "later" (when actually using the component - via the transclude-slots attribute).
* Using this mode requires clear contract agreement between the component and the consumers, but it enables the
* possibility of arbitrary slots being used (in number and names).
*
* For an example of the dynamic mode usage, see {@link ngTransclude#dynamic-multi-slot-transclusion}.
*
*
* #### Transclusion Functions
*
Expand Down Expand Up @@ -2085,6 +2094,13 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {

$template = jqLite(jqLiteClone(compileNode)).contents();

if (directiveValue === 'dynamic') {
if (!templateAttrs.transcludeSlots) {
throw $compileMinErr('reqdyn', 'Directive `{0}` requests dynamic transclusion slots but are not provided.', directive.name);
}
directiveValue = $parse(templateAttrs.transcludeSlots)();
}

if (isObject(directiveValue)) {

// We have transclusion slots,
Expand Down
50 changes: 50 additions & 0 deletions src/ng/directive/ngTransclude.js
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,56 @@
* expect(element(by.binding('text')).getText()).toEqual('TEXT');
* expect(element(by.css('.footer')).getText()).toEqual('Fallback Footer');
* });
* </file>
* </example>
*
* @example
* ### Dynamic multi-slot transclusion
* This example demonstrates using dynamic approach with multi-slot transclusion in a component directive.
* In this example, we use this mode to define a custom table component, while allowing the consumers to cleanly but
* optionally define their own cell templates:
* <example name="dynamicMultiSlotTranscludeExample" module="dynamicMultiSlotTranscludeExample">
* <file name="index.html">
* <style>
* .title, .footer {
* background-color: gray
* }
* </style>
* <div>
* <my-table transclude-slots="{ firstName: 'myFn', lastName: 'myLn' }"
* headers="[{name: 'firstName', caption: 'First Name'}, {name: 'middleName', caption: 'Middle Name'}, {name: 'lastName', caption: 'Last Name'}]"
* data="[{firstName: 'Ann', middleName: 'Margaret', lastName: 'Smith'}, {firstName: 'Edward', middleName: 'John', lastName: 'Williams'}]">
* <my-fn><strong>{{$parent.row['firstName']}}</strong></my-fn>
* <my-ln>{{$parent.row['lastName'].toUpperCase()}}</my-ln>
* </my-table>
* </div>
* </file>
* <file name="app.js">
* angular.module('dynamicMultiSlotTranscludeExample', [])
* .directive('myTable', function() {
* return {
* restrict: 'E',
* transclude: 'dynamic',
* template: '<table>' +
* '<thead>' +
* '<th ng-repeat="header in headers">{{header.caption}}</th>' +
* '</thead>' +
* '<tbody>' +
* '<tr ng-repeat="row in data">' +
* '<td ng-repeat="header in headers">' +
* '<div ng-if="transcludeSlots[header.name]"><div ng-transclude="{{header.name}}"></div></div>' +
* '<div ng-if="!transcludeSlots[header.name]">{{row[header.name]}}</div>' +
* '</td>' +
* '</tr>' +
* '</tbody>' +
* '</table>',
* scope: {
* transcludeSlots: '=',
* headers: '=',
* data: '='
* }
* };
* });
* </file>
* </example>
*/
Expand Down
80 changes: 80 additions & 0 deletions test/ng/compileSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -8843,6 +8843,86 @@ describe('$compile', function() {
});
});

describe('dynamic multi-slot transclude', function() {
it('should allow passing the transclude slots configuration - simple', function() {
module(function() {
directive('minionComponent', function() {
return {
restrict: 'E',
transclude: 'dynamic',
scope: {
transcludeSlots: '<'
},
template: '<div class="minions" ng-transclude="bobSlot"></div>'
};
});
});
inject(function($rootScope, $compile) {
element = $compile(
'<minion-component transclude-slots="{ bobSlot: \'bob\' }">' +
'<stuart><span>stuart</span></stuart>' +
'<kevin><span>kevin</span></kevin>' +
'<bob><span>bob</span></bob>' +
'<gru><span>gru</span></gru>' +
'</minion-component>')($rootScope);
$rootScope.$apply();
expect(element.text()).toEqual('bob');
});
});

it('should allow passing the transclude slots configuration - complex usage', function() {
module(function() {
directive('minionComponent', function() {
return {
restrict: 'E',
transclude: 'dynamic',
scope: {
transcludeSlots: '<'
},
template: '<div class="minions"><span ng-repeat="(slot, name) in transcludeSlots"><span ng-transclude="{{slot}}"></span>!</span></div>'
};
});
});
inject(function($rootScope, $compile) {
element = $compile(
'<minion-component transclude-slots="{ bobSlot: \'bob\', gruSlot: \'gru\', kevinSlot: \'kevin\', stuartSlot: \'stuart\' }">' +
'<stuart><span>stuart</span></stuart>' +
'<kevin><span>kevin</span></kevin>' +
'<bob><span>bob</span></bob>' +
'<gru><span>gru</span></gru>' +
'</minion-component>')($rootScope);
$rootScope.$apply();
expect(element.text()).toEqual('bob!gru!kevin!stuart!');
});
});

it('should error if slot config is not provided when using the directive', function() {
module(function() {
directive('minionComponent', function() {
return {
restrict: 'E',
transclude: 'dynamic',
scope: {
transcludeSlots: '<'
},
template: '<div class="minions"><span ng-repeat="(slot, name) in transcludeSlots"><span ng-transclude="{{slot}}"></span>!</span></div>'
};
});
});
inject(function($rootScope, $compile) {
expect(function() {
element = $compile(
'<minion-component>' +
'<stuart><span>stuart</span></stuart>' +
'<kevin><span>kevin</span></kevin>' +
'<bob><span>bob</span></bob>' +
'<gru><span>gru</span></gru>' +
'</minion-component>')($rootScope);
}).toThrowMinErr('$compile', 'reqdyn',
'Directive `minionComponent` requests dynamic transclusion slots but are not provided.');
});
});
});

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

Expand Down