From df45ab845f6a5560c64320a20f0e6bfe64f19bef Mon Sep 17 00:00:00 2001 From: Kara Erickson Date: Thu, 30 Apr 2015 20:26:30 -0700 Subject: [PATCH] feat($compile): add multi option to transclude property 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. --- .../content/error/$compile/invalidmulti.ngdoc | 9 +++ src/ng/compile.js | 56 ++++++++++++++++++- test/ng/compileSpec.js | 28 +++++++++- 3 files changed, 90 insertions(+), 3 deletions(-) create mode 100644 docs/content/error/$compile/invalidmulti.ngdoc diff --git a/docs/content/error/$compile/invalidmulti.ngdoc b/docs/content/error/$compile/invalidmulti.ngdoc new file mode 100644 index 000000000000..44fcbe9010f7 --- /dev/null +++ b/docs/content/error/$compile/invalidmulti.ngdoc @@ -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. + diff --git a/src/ng/compile.js b/src/ng/compile.js index 0aba65aec107..0bf133372c9c 100644 --- a/src/ng/compile.js +++ b/src/ng/compile.js @@ -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` @@ -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), @@ -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); @@ -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; @@ -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]; @@ -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(""); + 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. diff --git a/test/ng/compileSpec.js b/test/ng/compileSpec.js index b89f1d08312c..6120a57029da 100755 --- a/test/ng/compileSpec.js +++ b/test/ng/compileSpec.js @@ -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 { @@ -6585,6 +6585,32 @@ describe('$compile', function() { }); }); + describe('multi transclusion', function() { + beforeEach(module(function() { + directive('transclude', valueFn({ + transclude: 'multi', + scope: {}, + template: '
' + })); + })); + it('should append ng-transclude-to elements to matching ng-tranclude-ids', inject(function($compile, $rootScope) { + var topTarget, bottomTarget; + element = $compile( + '
In bottom.
In top.
' + )($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('
In bottom.
In top.
')($rootScope); + }).toThrowMinErr( + '$compile', 'invalidmulti', 'Invalid transclusion. Element
' + + 'does not match any known ng-transclude-select targets.'); + })); + }); describe('img[src] sanitization', function() {