diff --git a/docs/content/guide/directive.ngdoc b/docs/content/guide/directive.ngdoc index 22dd639ee970..cfc4af90e0ae 100644 --- a/docs/content/guide/directive.ngdoc +++ b/docs/content/guide/directive.ngdoc @@ -444,7 +444,11 @@ compiler}. The attributes are: * `true` - transclude the content of the directive. * `'element'` - transclude the whole element including any directives defined at lower priority. - + * `'multi-element'` - if the node where this directive is present contains an attribute + named `${directiveName}-start` (were `${directiveName}` is the name of the directive) then + the entire block of elements is transcluded until a sibling node is found with and attribute + named `${directiveName}-end`. If and attribute name `${directiveName}-start` is not found + then it behaves like the `'element'` transclude. * `compile`: This is the compile function described in the section below. diff --git a/src/ng/compile.js b/src/ng/compile.js index 96529d3cdda9..1bd5ecb38529 100644 --- a/src/ng/compile.js +++ b/src/ng/compile.js @@ -357,7 +357,7 @@ function $CompileProvider($provide) { //================================ - function compile($compileNodes, transcludeFn, maxPriority) { + function compile($compileNodes, transcludeFn, maxPriority, limitMaxPriorityToFirstElement) { if (!($compileNodes instanceof jqLite)) { // jquery always rewraps, whereas we need to preserve the original selector so that we can modify it. $compileNodes = jqLite($compileNodes); @@ -369,7 +369,8 @@ function $CompileProvider($provide) { $compileNodes[index] = jqLite(node).wrap('').parent()[0]; } }); - var compositeLinkFn = compileNodes($compileNodes, transcludeFn, $compileNodes, maxPriority); + var compositeLinkFn = compileNodes($compileNodes, transcludeFn, $compileNodes, maxPriority, + limitMaxPriorityToFirstElement); return function publicLinkFn(scope, cloneConnectFn){ assertArg(scope, 'scope'); // important!!: we must call our jqLite.clone() since the jQuery one is trying to be smart @@ -418,9 +419,13 @@ function $CompileProvider($provide) { * rootElement must be set the jqLite collection of the compile root. This is * needed so that the jqLite collection items can be replaced with widgets. * @param {number=} max directive priority + * @param {boolean=} limitMaxPriorityToFirstElement if the max priority should only apply to + * the first element in the list. A true value here will make the maxPriority only apply + * to the first element on the list while the other elements on the list will not have + * a maxPriority set * @returns {?function} A composite linking function of all of the matched directives or null. */ - function compileNodes(nodeList, transcludeFn, $rootElement, maxPriority) { + function compileNodes(nodeList, transcludeFn, $rootElement, maxPriority, limitMaxPriorityToFirstElement) { var linkFns = [], nodeLinkFn, childLinkFn, directives, attrs, linkFnFound; @@ -428,7 +433,8 @@ function $CompileProvider($provide) { attrs = new Attributes(); // we must always refer to nodeList[i] since the nodes can be replaced underneath us. - directives = collectDirectives(nodeList[i], [], attrs, maxPriority); + directives = collectDirectives(nodeList[i], [], attrs, + (limitMaxPriorityToFirstElement && i != 0) ? undefined : maxPriority); nodeLinkFn = (directives.length) ? applyDirectivesToNode(directives, nodeList[i], attrs, transcludeFn, $rootElement) @@ -645,6 +651,24 @@ function $CompileProvider($provide) { compileNode = $compileNode[0]; replaceWith(jqCollection, jqLite($template[0]), compileNode); childTranscludeFn = compile($template, transcludeFn, terminalPriority); + } else if (directiveValue == 'multi-element') { + // We need to compile a clone of the elements, but at the same time we have to be sure that these elements are siblings + var nestedContent = extractMultiElementTransclude(compileNode, directiveName); + var cloneContent = jqLite('
'); + + $template = jqLite(compileNode); + $compileNode = templateAttrs.$$element = + jqLite(document.createComment(' ' + directiveName + ': ' + templateAttrs[directiveName] + ' ')); + compileNode = $compileNode[0]; + replaceWith(jqCollection, jqLite($template[0]), compileNode); + cloneContent.append($template); + forEach(nestedContent.splice(1, nestedContent.length - 1), + function(toRemove) { + removeElement(jqCollection, toRemove); + cloneContent.append(toRemove); + } + ); + childTranscludeFn = compile(cloneContent.contents(), transcludeFn, terminalPriority, true); } else { $template = jqLite(JQLiteClone(compileNode)).contents(); $compileNode.html(''); // clear contents @@ -731,6 +755,39 @@ function $CompileProvider($provide) { //////////////////// + + function extractMultiElementTransclude(cursor, directiveName) { + var transcludeContent = [], + c, count = 0, + transcludeStart = directiveName + 'Start', + transcludeEnd = directiveName + 'End'; + + do { + if (containsAttr(cursor, transcludeStart)) count++; + if (containsAttr(cursor, transcludeEnd)) count--; + transcludeContent.push(cursor); + cursor = cursor.nextSibling; + } while(count > 0 && cursor); + if (count > 0) throw new Error('Unmatched ' + transcludeStart + '.'); + if (count < 0) throw new Error('Unexpected ' + transcludeEnd + '.'); + return transcludeContent; + } + + + function containsAttr(node, attributeName) { + var attr, attrs = node.attributes, length = attrs && attrs.length; + if ( length ) { + for (var j = 0; j < length; j++) { + attr = attrs[j]; + if (attr.specified && directiveNormalize(attr.name) == attributeName) { + return true; + } + } + } + return false; + } + + function addLinkFns(pre, post) { if (pre) { pre.require = directive.require; @@ -1152,6 +1209,23 @@ function $CompileProvider($provide) { newNode[jqLite.expando] = oldNode[jqLite.expando]; $element[0] = newNode; } + + + function removeElement($rootElement, element) { + var i, ii, parent = element.parentNode; + + if ($rootElement) { + for(i = 0, ii = $rootElement.length; i < ii; i++) { + if ($rootElement[i] == element) { + $rootElement.splice(i, 1); + break; + } + } + } + if (parent) { + parent.removeChild(element); + } + } }]; } diff --git a/src/ng/directive/ngRepeat.js b/src/ng/directive/ngRepeat.js index 330f6abb18cc..86544cf19814 100644 --- a/src/ng/directive/ngRepeat.js +++ b/src/ng/directive/ngRepeat.js @@ -145,7 +145,7 @@ var ngRepeatDirective = ['$parse', '$animator', function($parse, $animator) { var NG_REMOVED = '$$NG_REMOVED'; return { - transclude: 'element', + transclude: 'multi-element', priority: 1000, terminal: true, compile: function(element, attr, linker) { @@ -258,7 +258,7 @@ var ngRepeatDirective = ['$parse', '$animator', function($parse, $animator) { if (lastBlockMap.hasOwnProperty(key)) { block = lastBlockMap[key]; animate.leave(block.element); - block.element[0][NG_REMOVED] = true; + forEach(block.element, function(leavingElement) { leavingElement[NG_REMOVED] = true; }); block.scope.$destroy(); } } @@ -274,7 +274,7 @@ var ngRepeatDirective = ['$parse', '$animator', function($parse, $animator) { // associated scope/element childScope = block.scope; - nextCursor = cursor[0]; + nextCursor = cursor[cursor.length - 1]; do { nextCursor = nextCursor.nextSibling; } while(nextCursor && nextCursor[NG_REMOVED]); @@ -284,7 +284,7 @@ var ngRepeatDirective = ['$parse', '$animator', function($parse, $animator) { cursor = block.element; } else { // existing item which got moved - animate.move(block.element, null, cursor); + animate.move(block.element, null, cursor.eq(-1)); cursor = block.element; } } else { @@ -301,7 +301,7 @@ var ngRepeatDirective = ['$parse', '$animator', function($parse, $animator) { if (!block.element) { linker(childScope, function(clone) { - animate.enter(clone, null, cursor); + animate.enter(clone, null, cursor.eq(-1)); cursor = clone; block.scope = childScope; block.element = clone; diff --git a/test/ng/compileSpec.js b/test/ng/compileSpec.js index 26f61357bee3..2f5181db0675 100755 --- a/test/ng/compileSpec.js +++ b/test/ng/compileSpec.js @@ -381,6 +381,46 @@ describe('$compile', function() { }) }); + it("should complain when the multi-element end tag can't be found among one of the following siblings", inject(function($compile) { + forEach([ + // no content, no end tag + '', + + // content, no end tag + '' + + '{{item.text}}>' + + '{{item.done}}', + + // content, end tag too deep + '' + + '