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

Commit

Permalink
feat($compile): add multi option to transclude property
Browse files Browse the repository at this point in the history
Previously, there were two options to the transclude property: true and "element".
This commit adds a third option, "multi", that automatically matches transcluded elements
to elements in the directive template with matching ng-transclude-select selectors.
  • Loading branch information
Kara Erickson committed May 1, 2015
1 parent c10b249 commit df45ab8
Show file tree
Hide file tree
Showing 3 changed files with 90 additions and 3 deletions.
9 changes: 9 additions & 0 deletions docs/content/error/$compile/invalidmulti.ngdoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
@ngdoc error
@name $compile:invalidmulti
@fullName Invalid multi transclusion.
@description

When using a directive that allows multiple transclusion points (e.g. the `transclude` property is set to `multi` in the directive definition), each transcluded element needs to match an `ng-transclude-select` selector in the directive template. Otherwise, it won't be properly appended to the DOM.

This error occurs when the element does not match `ng-transclude-select` selectors in the directive template.

56 changes: 54 additions & 2 deletions src/ng/compile.js
Original file line number Diff line number Diff line change
Expand Up @@ -316,13 +316,16 @@
* The contents are compiled and provided to the directive as a **transclusion function**. See the
* {@link $compile#transclusion Transclusion} section below.
*
* There are two kinds of transclusion depending upon whether you want to transclude just the contents of the
* directive's element or the entire element:
* 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 transclude to multiple points in the directive.
*
* * `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.
* * `'multi'` - allows transclusion into multiple points of the directive's template. When used, automatically
* appends any transcluded elements that match `ng-transclude-select` selector to the `ng-transclude-select` element
* in the directive template.
*
*
* #### `compile`
Expand Down Expand Up @@ -1645,6 +1648,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
templateDirective = previousCompileContext.templateDirective,
nonTlbTranscludeDirective = previousCompileContext.nonTlbTranscludeDirective,
hasTranscludeDirective = false,
hasMultiTranscludeDirective = false,
hasTemplate = false,
hasElementTranscludeDirective = previousCompileContext.hasElementTranscludeDirective,
$compileNode = templateAttrs.$$element = jqLite(compileNode),
Expand Down Expand Up @@ -1737,6 +1741,9 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
nonTlbTranscludeDirective: nonTlbTranscludeDirective
});
} else {
if (directiveValue == 'multi') {
hasMultiTranscludeDirective = true;
}
$template = jqLite(jqLiteClone(compileNode)).contents();
$compileNode.empty(); // clear contents
childTranscludeFn = compile($template, transcludeFn);
Expand Down Expand Up @@ -1833,6 +1840,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
nodeLinkFn.scope = newScopeDirective && newScopeDirective.scope === true;
nodeLinkFn.transcludeOnThisElement = hasTranscludeDirective;
nodeLinkFn.elementTranscludeOnThisElement = hasElementTranscludeDirective;
nodeLinkFn.multiTranscludeOnThisElement = hasMultiTranscludeDirective;
nodeLinkFn.templateOnThisElement = hasTemplate;
nodeLinkFn.transclude = childTranscludeFn;

Expand Down Expand Up @@ -2027,6 +2035,8 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
}
childLinkFn && childLinkFn(scopeToChild, linkNode.childNodes, undefined, boundTranscludeFn);

if (thisLinkFn.multiTranscludeOnThisElement) multiTransclude($element[0], transcludeFn);

// POSTLINKING
for (i = postLinkFns.length - 1; i >= 0; i--) {
linkFn = postLinkFns[i];
Expand Down Expand Up @@ -2128,6 +2138,48 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
return false;
}

/**
* Matches elements in the transcluded content to elements in the directive template by ng-transclude-selector.
* Used in directives that set transclude to 'multi'.
*
* @param {jqLite} dirElement Main directive element
* @param {function()} transcludeFn Transclusion function for the directive
**/
function multiTransclude(dirElement, transcludeFn) {
transcludeFn(transcludeCallback);

function transcludeCallback(clone) {
var target,
selector,
selectedElements,
transcludeTargets = dirElement.querySelectorAll('[ng-transclude-select]'),
cloneWrapper = jqLite("<span></span>");
cloneWrapper.append(clone);

for (var i = 0, ii = transcludeTargets.length; i < ii; i++) {
target = jqLite(transcludeTargets[i]);
selector = target.attr('ng-transclude-select');
selectedElements = cloneWrapper[0].querySelectorAll(selector);
if (selectedElements.length) target.append(selectedElements);
}
checkForTranscludeErr(cloneWrapper);
cloneWrapper.remove();
}

function checkForTranscludeErr(cloneWrapper) {
var orphanElement;
if (cloneWrapper.children().length) {
orphanElement = jqLite(cloneWrapper.children()[0]);
cloneWrapper.children().remove();
throw $compileMinErr('invalidmulti',
'Invalid transclusion. Element {0} does not match any known ng-transclude-select targets.',
startingTag(orphanElement));
}

}
}


/**
* When the element is replaced with HTML template then the new attributes
* on the template need to be merged with the existing attributes in the DOM.
Expand Down
28 changes: 27 additions & 1 deletion test/ng/compileSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -6311,7 +6311,7 @@ describe('$compile', function() {
});


it('should terminate compilation only for element trasclusion', function() {
it('should terminate compilation only for element transclusion', function() {
module(function() {
directive('elementTrans', function(log) {
return {
Expand Down Expand Up @@ -6585,6 +6585,32 @@ describe('$compile', function() {
});
});

describe('multi transclusion', function() {
beforeEach(module(function() {
directive('transclude', valueFn({
transclude: 'multi',
scope: {},
template: '<div ng-transclude-select="[top]"></div><div ng-transclude-select="[bottom]"></div>'
}));
}));
it('should append ng-transclude-to elements to matching ng-tranclude-ids', inject(function($compile, $rootScope) {
var topTarget, bottomTarget;
element = $compile(
'<div transclude><div bottom>In bottom.</div><div top>In top.</div></div></div>'
)($rootScope);
topTarget = jqLite(element[0].querySelector('[ng-transclude-select="[top]"]'));
bottomTarget = jqLite(element[0].querySelector('[ng-transclude-select="[bottom]"]'));
expect(topTarget.text()).toEqual('In top.');
expect(bottomTarget.text()).toEqual('In bottom.');
}));
it('should throw error if transcluded element does not match any ng-transclude-selects', inject(function($compile, $rootScope) {
expect(function() {
$compile('<div transclude><div>In bottom.</div><div>In top.</div></div>')($rootScope);
}).toThrowMinErr(
'$compile', 'invalidmulti', 'Invalid transclusion. Element <div class="ng-scope"> ' +
'does not match any known ng-transclude-select targets.');
}));
});

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

Expand Down

0 comments on commit df45ab8

Please sign in to comment.