diff --git a/src/jqLite.js b/src/jqLite.js
index cf9d1fa168e8..4959a2ed36b9 100644
--- a/src/jqLite.js
+++ b/src/jqLite.js
@@ -165,7 +165,8 @@ function JQLite(element) {
div.innerHTML = '
' + element; // IE insanity to make NoScope elements work!
div.removeChild(div.firstChild); // remove the superfluous div
JQLiteAddNodes(this, div.childNodes);
- this.remove(); // detach the elements from the temporary DOM div.
+ var fragment = jqLite(document.createDocumentFragment());
+ fragment.append(this); // detach the elements from the temporary DOM div.
} else {
JQLiteAddNodes(this, element);
}
@@ -456,24 +457,26 @@ forEach({
}
},
- text: extend((msie < 9)
- ? function(element, value) {
- if (element.nodeType == 1 /** Element */) {
- if (isUndefined(value))
- return element.innerText;
- element.innerText = value;
- } else {
- if (isUndefined(value))
- return element.nodeValue;
- element.nodeValue = value;
- }
+ text: (function() {
+ var NODE_TYPE_TEXT_PROPERTY = [];
+ if (msie < 9) {
+ NODE_TYPE_TEXT_PROPERTY[1] = 'innerText'; /** Element **/
+ NODE_TYPE_TEXT_PROPERTY[3] = 'nodeValue'; /** Text **/
+ } else {
+ NODE_TYPE_TEXT_PROPERTY[1] = /** Element **/
+ NODE_TYPE_TEXT_PROPERTY[3] = 'textContent'; /** Text **/
+ }
+ getText.$dv = '';
+ return getText;
+
+ function getText(element, value) {
+ var textProp = NODE_TYPE_TEXT_PROPERTY[element.nodeType]
+ if (isUndefined(value)) {
+ return textProp ? element[textProp] : '';
}
- : function(element, value) {
- if (isUndefined(value)) {
- return element.textContent;
- }
- element.textContent = value;
- }, {$dv:''}),
+ element[textProp] = value;
+ }
+ })(),
val: function(element, value) {
if (isUndefined(value)) {
@@ -518,8 +521,14 @@ forEach({
return this;
} else {
// we are a read, so read the first child.
- if (this.length)
- return fn(this[0], arg1, arg2);
+ var value = fn.$dv;
+ // Only if we have $dv do we iterate over all, otherwise it is just the first element.
+ var jj = value == undefined ? Math.min(this.length, 1) : this.length;
+ for (var j = 0; j < jj; j++) {
+ var nodeValue = fn(this[j], arg1, arg2);
+ value = value ? value + nodeValue : nodeValue;
+ }
+ return value;
}
} else {
// we are a write, so apply to all children
@@ -529,7 +538,6 @@ forEach({
// return self for chaining
return this;
}
- return fn.$dv;
};
});
diff --git a/src/ng/animator.js b/src/ng/animator.js
index 2965717bdc3d..2b399813264b 100644
--- a/src/ng/animator.js
+++ b/src/ng/animator.js
@@ -395,11 +395,16 @@ var $AnimatorProvider = function() {
}
function insert(element, parent, after) {
- if (after) {
- after.after(element);
- } else {
- parent.append(element);
- }
+ var afterNode = after && after[after.length - 1];
+ var parentNode = parent && parent[0] || afterNode && afterNode.parentNode;
+ var afterNextSibling = afterNode && afterNode.nextSibling;
+ forEach(element, function(node) {
+ if (afterNextSibling) {
+ parentNode.insertBefore(node, afterNextSibling);
+ } else {
+ parentNode.appendChild(node);
+ }
+ });
}
function remove(element) {
diff --git a/src/ng/compile.js b/src/ng/compile.js
index be22482b033b..2dddf82dcbff 100644
--- a/src/ng/compile.js
+++ b/src/ng/compile.js
@@ -358,11 +358,12 @@ function $CompileProvider($provide) {
// jquery always rewraps, whereas we need to preserve the original selector so that we can modify it.
$compileNodes = jqLite($compileNodes);
}
+ var tempParent = document.createDocumentFragment();
// We can not compile top level text elements since text nodes can be merged and we will
// not be able to attach scope data to them, so we will wrap them in
forEach($compileNodes, function(node, index){
if (node.nodeType == 3 /* text node */ && node.nodeValue.match(/\S+/) /* non-empty */ ) {
- $compileNodes[index] = jqLite(node).wrap('').parent()[0];
+ $compileNodes[index] = node = jqLite(node).wrap('').parent()[0];
}
});
var compositeLinkFn = compileNodes($compileNodes, transcludeFn, $compileNodes, maxPriority);
@@ -420,7 +421,7 @@ 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, i == 0 ? maxPriority : undefined);
nodeLinkFn = (directives.length)
? applyDirectivesToNode(directives, nodeList[i], attrs, transcludeFn, $rootElement)
@@ -509,6 +510,10 @@ function $CompileProvider($provide) {
// iterate over the attributes
for (var attr, name, nName, ngAttrName, value, nAttrs = node.attributes,
j = 0, jj = nAttrs && nAttrs.length; j < jj; j++) {
+ var attrStartName;
+ var attrEndName;
+ var index;
+
attr = nAttrs[j];
if (attr.specified) {
name = attr.name;
@@ -517,6 +522,11 @@ function $CompileProvider($provide) {
if (NG_ATTR_BINDING.test(ngAttrName)) {
name = ngAttrName.substr(6).toLowerCase();
}
+ if ((index = ngAttrName.lastIndexOf('Start')) != -1 && index == ngAttrName.length - 5) {
+ attrStartName = name;
+ attrEndName = name.substr(0, name.length - 5) + 'end';
+ name = name.substr(0, name.length - 6);
+ }
nName = directiveNormalize(name.toLowerCase());
attrsMap[nName] = name;
attrs[nName] = value = trim((msie && name == 'href')
@@ -526,7 +536,7 @@ function $CompileProvider($provide) {
attrs[nName] = true; // presence means true
}
addAttrInterpolateDirective(node, directives, value, nName);
- addDirective(directives, nName, 'A', maxPriority);
+ addDirective(directives, nName, 'A', maxPriority, attrStartName, attrEndName);
}
}
@@ -565,6 +575,47 @@ function $CompileProvider($provide) {
return directives;
}
+ /**
+ * Given a node with an directive-start it collects all of the siblings until it find directive-end.
+ * @param node
+ * @param attrStart
+ * @param attrEnd
+ * @returns {*}
+ */
+ function groupScan(node, attrStart, attrEnd) {
+ var nodes = [];
+ var depth = 0;
+ if (attrStart && node.hasAttribute && node.hasAttribute(attrStart)) {
+ var startNode = node;
+ do {
+ if (!node) {
+ throw ngError(51, "Unterminated attribute, found '{0}' but no matching '{1}' found.", attrStart, attrEnd);
+ }
+ if (node.hasAttribute(attrStart)) depth++;
+ if (node.hasAttribute(attrEnd)) depth--;
+ nodes.push(node);
+ node = node.nextSibling;
+ } while (depth > 0);
+ } else {
+ nodes.push(node);
+ }
+ return jqLite(nodes);
+ }
+
+ /**
+ * Wrapper for linking function which converts normal linking function into a grouped
+ * linking function.
+ * @param linkFn
+ * @param attrStart
+ * @param attrEnd
+ * @returns {Function}
+ */
+ function groupElementsLinkFnWrapper(linkFn, attrStart, attrEnd) {
+ return function(scope, element, attrs, controllers) {
+ element = groupScan(element[0], attrStart, attrEnd);
+ return linkFn(scope, element, attrs, controllers);
+ }
+ }
/**
* Once the directives have been collected, their compile functions are executed. This method
@@ -601,6 +652,13 @@ function $CompileProvider($provide) {
// executes all directives on the current element
for(var i = 0, ii = directives.length; i < ii; i++) {
directive = directives[i];
+ var attrStart = directive.$$start;
+ var attrEnd = directive.$$end;
+
+ // collect multiblock sections
+ if (attrStart) {
+ $compileNode = groupScan(compileNode, attrStart, attrEnd)
+ }
$template = undefined;
if (terminalPriority > directive.priority) {
@@ -631,11 +689,11 @@ function $CompileProvider($provide) {
transcludeDirective = directive;
terminalPriority = directive.priority;
if (directiveValue == 'element') {
- $template = jqLite(compileNode);
+ $template = groupScan(compileNode, attrStart, attrEnd)
$compileNode = templateAttrs.$$element =
jqLite(document.createComment(' ' + directiveName + ': ' + templateAttrs[directiveName] + ' '));
compileNode = $compileNode[0];
- replaceWith(jqCollection, jqLite($template[0]), compileNode);
+ replaceWith(jqCollection, jqLite(sliceArgs($template)), compileNode);
childTranscludeFn = compile($template, transcludeFn, terminalPriority);
} else {
$template = jqLite(JQLiteClone(compileNode)).contents();
@@ -699,9 +757,9 @@ function $CompileProvider($provide) {
try {
linkFn = directive.compile($compileNode, templateAttrs, childTranscludeFn);
if (isFunction(linkFn)) {
- addLinkFns(null, linkFn);
+ addLinkFns(null, linkFn, attrStart, attrEnd);
} else if (linkFn) {
- addLinkFns(linkFn.pre, linkFn.post);
+ addLinkFns(linkFn.pre, linkFn.post, attrStart, attrEnd);
}
} catch (e) {
$exceptionHandler(e, startingTag($compileNode));
@@ -723,12 +781,14 @@ function $CompileProvider($provide) {
////////////////////
- function addLinkFns(pre, post) {
+ function addLinkFns(pre, post, attrStart, attrEnd) {
if (pre) {
+ if (attrStart) pre = groupElementsLinkFnWrapper(pre, attrStart, attrEnd);
pre.require = directive.require;
preLinkFns.push(pre);
}
if (post) {
+ if (attrStart) post = groupElementsLinkFnWrapper(post, attrStart, attrEnd);
post.require = directive.require;
postLinkFns.push(post);
}
@@ -907,8 +967,8 @@ function $CompileProvider($provide) {
* * `M`: comment
* @returns true if directive was added.
*/
- function addDirective(tDirectives, name, location, maxPriority) {
- var match = false;
+ function addDirective(tDirectives, name, location, maxPriority, startAttrName, endAttrName) {
+ var match = null;
if (hasDirectives.hasOwnProperty(name)) {
for(var directive, directives = $injector.get(name + Suffix),
i = 0, ii = directives.length; i directive.priority) &&
directive.restrict.indexOf(location) != -1) {
+ if (startAttrName) {
+ directive = inherit(directive, {$$start: startAttrName, $$end: endAttrName});
+ }
tDirectives.push(directive);
- match = true;
+ match = directive;
}
} catch(e) { $exceptionHandler(e); }
}
@@ -1120,30 +1183,50 @@ function $CompileProvider($provide) {
*
* @param {JqLite=} $rootElement The root of the compile tree. Used so that we can replace nodes
* in the root of the tree.
- * @param {JqLite} $element The jqLite element which we are going to replace. We keep the shell,
+ * @param {JqLite} elementsToRemove The jqLite element which we are going to replace. We keep the shell,
* but replace its DOM node reference.
* @param {Node} newNode The new DOM node.
*/
- function replaceWith($rootElement, $element, newNode) {
- var oldNode = $element[0],
- parent = oldNode.parentNode,
+ function replaceWith($rootElement, elementsToRemove, newNode) {
+ var firstElementToRemove = elementsToRemove[0],
+ removeCount = elementsToRemove.length,
+ parent = firstElementToRemove.parentNode,
i, ii;
if ($rootElement) {
for(i = 0, ii = $rootElement.length; i < ii; i++) {
- if ($rootElement[i] == oldNode) {
- $rootElement[i] = newNode;
+ if ($rootElement[i] == firstElementToRemove) {
+ $rootElement[i++] = newNode;
+ for (var j = i, j2 = j + removeCount - 1,
+ jj = $rootElement.length;
+ j < jj; j++, j2++) {
+ if (j2 < jj) {
+ $rootElement[j] = $rootElement[j2];
+ } else {
+ delete $rootElement[j];
+ }
+ }
+ $rootElement.length -= removeCount - 1;
break;
}
}
}
if (parent) {
- parent.replaceChild(newNode, oldNode);
+ parent.replaceChild(newNode, firstElementToRemove);
+ }
+ var fragment = document.createDocumentFragment();
+ fragment.appendChild(firstElementToRemove);
+ newNode[jqLite.expando] = firstElementToRemove[jqLite.expando];
+ for (var k = 1, kk = elementsToRemove.length; k < kk; k++) {
+ var element = elementsToRemove[k];
+ jqLite(element).remove(); // must do this way to clean up expando
+ fragment.appendChild(element);
+ delete elementsToRemove[k];
}
- newNode[jqLite.expando] = oldNode[jqLite.expando];
- $element[0] = newNode;
+ elementsToRemove[0] = newNode;
+ elementsToRemove.length = 1
}
}];
}
diff --git a/src/ng/directive/ngRepeat.js b/src/ng/directive/ngRepeat.js
index 34d32f59adb2..6c2da071956d 100644
--- a/src/ng/directive/ngRepeat.js
+++ b/src/ng/directive/ngRepeat.js
@@ -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(element) { element[NG_REMOVED] = true});
block.scope.$destroy();
}
}
diff --git a/test/jqLiteSpec.js b/test/jqLiteSpec.js
index 1ebe6ad48943..70c18d35600f 100644
--- a/test/jqLiteSpec.js
+++ b/test/jqLiteSpec.js
@@ -56,6 +56,9 @@ describe('jqLite', function() {
it('should allow construction with html', function() {
var nodes = jqLite('1
2');
+ expect(nodes[0].parentNode).toBeDefined();
+ expect(nodes[0].parentNode.nodeType).toBe(11); /** Document Fragment **/;
+ expect(nodes[0].parentNode).toBe(nodes[1].parentNode);
expect(nodes.length).toEqual(2);
expect(nodes[0].innerHTML).toEqual('1');
expect(nodes[1].innerHTML).toEqual('2');
@@ -644,12 +647,13 @@ describe('jqLite', function() {
it('should read/write value', function() {
- var element = jqLite('abc
');
- expect(element.length).toEqual(1);
- expect(element[0].innerHTML).toEqual('abc');
+ var element = jqLite('ab
c');
+ expect(element.length).toEqual(2);
+ expect(element[0].innerHTML).toEqual('ab');
+ expect(element[1].innerHTML).toEqual('c');
expect(element.text()).toEqual('abc');
expect(element.text('xyz') == element).toBeTruthy();
- expect(element.text()).toEqual('xyz');
+ expect(element.text()).toEqual('xyzxyz');
});
});
diff --git a/test/ng/compileSpec.js b/test/ng/compileSpec.js
index bf3d0b777c50..95b2ab72d93f 100755
--- a/test/ng/compileSpec.js
+++ b/test/ng/compileSpec.js
@@ -2718,4 +2718,129 @@ describe('$compile', function() {
expect(element.attr('test4')).toBe('Misko');
}));
});
+
+
+ describe('multi-element directive', function() {
+ it('should group on link function', inject(function($compile, $rootScope) {
+ $rootScope.show = false;
+ element = $compile(
+ '' +
+ '' +
+ '' +
+ '
')($rootScope);
+ $rootScope.$digest();
+ var spans = element.find('span');
+ expect(spans.eq(0).css('display')).toBe('none');
+ expect(spans.eq(1).css('display')).toBe('none');
+ }));
+
+
+ it('should group on compile function', inject(function($compile, $rootScope) {
+ $rootScope.show = false;
+ element = $compile(
+ '' +
+ '{{i}}A' +
+ '{{i}}B;' +
+ '
')($rootScope);
+ $rootScope.$digest();
+ expect(element.text()).toEqual('1A1B;2A2B;');
+ }));
+
+
+ it('should group on $root compile function', inject(function($compile, $rootScope) {
+ $rootScope.show = false;
+ element = $compile(
+ '' +
+ '{{i}}A' +
+ '{{i}}B;' +
+ '')($rootScope);
+ $rootScope.$digest();
+ element = jqLite(element[0].parentNode.childNodes); // reset because repeater is top level.
+ expect(element.text()).toEqual('1A1B;2A2B;');
+ }));
+
+
+ it('should group on nested groups', inject(function($compile, $rootScope) {
+ $rootScope.show = false;
+ element = $compile(
+ '' +
+ '{{i}}A
' +
+ '' +
+ '' +
+ '{{i}}B;
' +
+ '')($rootScope);
+ $rootScope.$digest();
+ element = jqLite(element[0].parentNode.childNodes); // reset because repeater is top level.
+ expect(element.text()).toEqual('1A..1B;2A..2B;');
+ }));
+
+
+ it('should group on nested groups', inject(function($compile, $rootScope) {
+ $rootScope.show = false;
+ element = $compile(
+ '' +
+ '{{i}}(
' +
+ '{{j}}-' +
+ '{{j}}' +
+ '){{i}};
' +
+ '')($rootScope);
+ $rootScope.$digest();
+ element = jqLite(element[0].parentNode.childNodes); // reset because repeater is top level.
+ expect(element.text()).toEqual('1(2-23-3)1;2(2-23-3)2;');
+ }));
+
+
+ it('should throw error if unterminated', function () {
+ module(function($compileProvider) {
+ $compileProvider.directive('foo', function() {
+ return {
+ };
+ });
+ });
+ inject(function($compile, $rootScope) {
+ expect(function() {
+ element = $compile(
+ '' +
+ '' +
+ '
');
+ }).toThrow("[NgErr51] Unterminated attribute, found 'foo-start' but no matching 'foo-end' found.");
+ });
+ });
+
+
+ it('should throw error if unterminated', function () {
+ module(function($compileProvider) {
+ $compileProvider.directive('foo', function() {
+ return {
+ };
+ });
+ });
+ inject(function($compile, $rootScope) {
+ expect(function() {
+ element = $compile(
+ '' +
+ '' +
+ '
');
+ }).toThrow("[NgErr51] Unterminated attribute, found 'foo-start' but no matching 'foo-end' found.");
+ });
+ });
+
+
+ it('should support data- and x- prefix', inject(function($compile, $rootScope) {
+ $rootScope.show = false;
+ element = $compile(
+ '' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '
')($rootScope);
+ $rootScope.$digest();
+ var spans = element.find('span');
+ expect(spans.eq(0).css('display')).toBe('none');
+ expect(spans.eq(1).css('display')).toBe('none');
+ expect(spans.eq(2).css('display')).toBe('none');
+ expect(spans.eq(3).css('display')).toBe('none');
+ }));
+ });
});