Permalink
Browse files

feat($compile): support multi-element directive

By appending  directive-start and directive-end to a
directive it is now possible to have the directive
act on a group of elements.

It is now possible to iterate over multiple elements like so:

<table>
  <tr ng-repeat-start="item in list">I get repeated</tr>
  <tr ng-repeat-end>I also get repeated</tr>
</table>
1 parent b8ea7f6 commit e46100f7097d9a8f174bdb9e15d4c6098395c3f2 @mhevery mhevery committed May 24, 2013
Showing with 276 additions and 51 deletions.
  1. +29 −21 src/jqLite.js
  2. +10 −5 src/ng/animator.js
  3. +103 −20 src/ng/compile.js
  4. +1 −1 src/ng/directive/ngRepeat.js
  5. +8 −4 test/jqLiteSpec.js
  6. +125 −0 test/ng/compileSpec.js
View
@@ -165,7 +165,8 @@ function JQLite(element) {
div.innerHTML = '<div>&#160;</div>' + 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;
};
});
View
@@ -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) {
View
@@ -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 <span>
forEach($compileNodes, function(node, index){
if (node.nodeType == 3 /* text node */ && node.nodeValue.match(/\S+/) /* non-empty */ ) {
- $compileNodes[index] = jqLite(node).wrap('<span></span>').parent()[0];
+ $compileNodes[index] = node = jqLite(node).wrap('<span></span>').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,17 +967,20 @@ 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<ii; i++) {
try {
directive = directives[i];
if ( (maxPriority === undefined || maxPriority > 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
}
}];
}
@@ -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();
}
}
View
@@ -56,6 +56,9 @@ describe('jqLite', function() {
it('should allow construction with html', function() {
var nodes = jqLite('<div>1</div><span>2</span>');
+ 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('<div>abc</div>');
- expect(element.length).toEqual(1);
- expect(element[0].innerHTML).toEqual('abc');
+ var element = jqLite('<div>ab</div><span>c</span>');
+ 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');
});
});
Oops, something went wrong.

7 comments on commit e46100f

VonD commented on e46100f Jun 10, 2013

hi,
thanks for this great feature. Do you have an idea when this will be in the stable branch ?
thanks a lot.

That is super. We'll be watching for this on the mainline, too 👍 .

Nice change, I've got a couple of use cases for this.

Can not wait to use it!

fizx replied Aug 13, 2013

AFICT, this doesn't help a case where you want nested repeats of different cardinality, e.g. in bastardized regex: (h4 p_)_.

This is neato! One thought though - it is encroaching on what attributes you can and can't use - even when they are custom attributes.

kendo-labs/angular-kendo#203 (comment)

Is there a way to override this behavior (turn it on/off), or are we now going to have to change any start end named attributes?

Contributor

caitp replied Feb 17, 2014

@burkeholland There's a PR to provide a bit more control for this, #5372 --- for the time being, unfortunately you are stuck with it =(

Please sign in to comment.