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
with "ng-transclude-to" attributes to directive template elements with "ng-transclude-id" attributes.
  • Loading branch information
Kara Erickson committed Apr 27, 2015
1 parent c10b249 commit d276c91
Show file tree
Hide file tree
Showing 3 changed files with 86 additions and 3 deletions.
9 changes: 9 additions & 0 deletions docs/content/error/$compile/notranscludetarget.ngdoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
@ngdoc error
@name $compile:notranscludetarget
@fullName Invalid ng-transclude-to attribute
@description

When using a directive that allows multiple transclusion points (e.g. the `transclude` property is set to `multi` in the directive definition), a valid `ng-transclude-to` attribute is required on each transcluded element in order to attach it to the right place in the DOM.

This error occurs when the `ng-transclude-to` attribute is either missing or cannot be matched to a `ng-transclude-id` inside the directive template.

47 changes: 45 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 with `ng-transclude-to` attributes to elements in the directive's template with
* corresponding `ng-transclude-id` attributes.
*
*
* #### `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, transcludeFn);

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

/**
* Matches elements in the transcluded content to elements in the directive template by ng-transclude-id.
* 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 cloneElement, targetID, target;

for (var i = 0, ii = clone.length; i < ii; i++) {
cloneElement = clone[i];
if (cloneElement.nodeType === NODE_TYPE_TEXT) continue;
targetID = cloneElement.getAttribute('ng-transclude-to');
target = jqLite(dirElement[0].querySelector('[ng-transclude-id="' + targetID + '"]'));
checkForTranscludeErr(target, cloneElement, targetID);
target.append(cloneElement);
}
}
}

function checkForTranscludeErr(target, cloneElement, targetID) {
if (!target.length) {
jqLite(cloneElement).remove();
throw $compileMinErr('notranscludetarget',
'Target {0} is not valid. Please specify a proper ng-transclude-to attribute for <{1}>{2}</{1}>.',
targetID, cloneElement.tagName.toLowerCase(), cloneElement.innerHTML);
}
}

/**
* 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
33 changes: 32 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,37 @@ describe('$compile', function() {
});
});

describe('multi transclusion', function() {
beforeEach(module(function() {
directive('transclude', valueFn({
transclude: 'multi',
scope: {},
template: '<div ng-transclude-id="top"></div><div ng-transclude-id="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 ng-transclude-to="bottom">In bottom.</div><div ng-transclude-to="top">In top.</div></div></div>'
)($rootScope);
topTarget = jqLite(element[0].querySelector('[ng-transclude-id="top"]'));
bottomTarget = jqLite(element[0].querySelector('[ng-transclude-id="bottom"]'));
expect(topTarget.text()).toEqual('In top.');
expect(bottomTarget.text()).toEqual('In bottom.');
}));
it('should throw error if ng-transclude-to attribute is missing or invalid', inject(function($compile, $rootScope) {
expect(function() {
$compile('<div transclude><div>In bottom.</div></div>')($rootScope);
}).toThrowMinErr(
"$compile", "notranscludetarget", "Target null is not valid. Please specify a proper " +
"ng-transclude-to attribute for <div>In bottom.</div>.");
expect(function() {
$compile('<div transclude><div ng-transclude-to="wrong-target">In bottom.</div></div>')($rootScope);
}).toThrowMinErr(
"$compile", "notranscludetarget", "Target wrong-target is not valid. Please specify a proper " +
"ng-transclude-to attribute for <div>In bottom.</div>.");
}));
});

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

Expand Down

0 comments on commit d276c91

Please sign in to comment.