Skip to content
This repository was archived by the owner on Apr 12, 2024. It is now read-only.

Commit e46100f

Browse files
committed
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 e46100f

File tree

6 files changed

+276
-51
lines changed

6 files changed

+276
-51
lines changed

src/jqLite.js

+29-21
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,8 @@ function JQLite(element) {
165165
div.innerHTML = '<div>&#160;</div>' + element; // IE insanity to make NoScope elements work!
166166
div.removeChild(div.firstChild); // remove the superfluous div
167167
JQLiteAddNodes(this, div.childNodes);
168-
this.remove(); // detach the elements from the temporary DOM div.
168+
var fragment = jqLite(document.createDocumentFragment());
169+
fragment.append(this); // detach the elements from the temporary DOM div.
169170
} else {
170171
JQLiteAddNodes(this, element);
171172
}
@@ -456,24 +457,26 @@ forEach({
456457
}
457458
},
458459

459-
text: extend((msie < 9)
460-
? function(element, value) {
461-
if (element.nodeType == 1 /** Element */) {
462-
if (isUndefined(value))
463-
return element.innerText;
464-
element.innerText = value;
465-
} else {
466-
if (isUndefined(value))
467-
return element.nodeValue;
468-
element.nodeValue = value;
469-
}
460+
text: (function() {
461+
var NODE_TYPE_TEXT_PROPERTY = [];
462+
if (msie < 9) {
463+
NODE_TYPE_TEXT_PROPERTY[1] = 'innerText'; /** Element **/
464+
NODE_TYPE_TEXT_PROPERTY[3] = 'nodeValue'; /** Text **/
465+
} else {
466+
NODE_TYPE_TEXT_PROPERTY[1] = /** Element **/
467+
NODE_TYPE_TEXT_PROPERTY[3] = 'textContent'; /** Text **/
468+
}
469+
getText.$dv = '';
470+
return getText;
471+
472+
function getText(element, value) {
473+
var textProp = NODE_TYPE_TEXT_PROPERTY[element.nodeType]
474+
if (isUndefined(value)) {
475+
return textProp ? element[textProp] : '';
470476
}
471-
: function(element, value) {
472-
if (isUndefined(value)) {
473-
return element.textContent;
474-
}
475-
element.textContent = value;
476-
}, {$dv:''}),
477+
element[textProp] = value;
478+
}
479+
})(),
477480

478481
val: function(element, value) {
479482
if (isUndefined(value)) {
@@ -518,8 +521,14 @@ forEach({
518521
return this;
519522
} else {
520523
// we are a read, so read the first child.
521-
if (this.length)
522-
return fn(this[0], arg1, arg2);
524+
var value = fn.$dv;
525+
// Only if we have $dv do we iterate over all, otherwise it is just the first element.
526+
var jj = value == undefined ? Math.min(this.length, 1) : this.length;
527+
for (var j = 0; j < jj; j++) {
528+
var nodeValue = fn(this[j], arg1, arg2);
529+
value = value ? value + nodeValue : nodeValue;
530+
}
531+
return value;
523532
}
524533
} else {
525534
// we are a write, so apply to all children
@@ -529,7 +538,6 @@ forEach({
529538
// return self for chaining
530539
return this;
531540
}
532-
return fn.$dv;
533541
};
534542
});
535543

src/ng/animator.js

+10-5
Original file line numberDiff line numberDiff line change
@@ -395,11 +395,16 @@ var $AnimatorProvider = function() {
395395
}
396396

397397
function insert(element, parent, after) {
398-
if (after) {
399-
after.after(element);
400-
} else {
401-
parent.append(element);
402-
}
398+
var afterNode = after && after[after.length - 1];
399+
var parentNode = parent && parent[0] || afterNode && afterNode.parentNode;
400+
var afterNextSibling = afterNode && afterNode.nextSibling;
401+
forEach(element, function(node) {
402+
if (afterNextSibling) {
403+
parentNode.insertBefore(node, afterNextSibling);
404+
} else {
405+
parentNode.appendChild(node);
406+
}
407+
});
403408
}
404409

405410
function remove(element) {

src/ng/compile.js

+103-20
Original file line numberDiff line numberDiff line change
@@ -358,11 +358,12 @@ function $CompileProvider($provide) {
358358
// jquery always rewraps, whereas we need to preserve the original selector so that we can modify it.
359359
$compileNodes = jqLite($compileNodes);
360360
}
361+
var tempParent = document.createDocumentFragment();
361362
// We can not compile top level text elements since text nodes can be merged and we will
362363
// not be able to attach scope data to them, so we will wrap them in <span>
363364
forEach($compileNodes, function(node, index){
364365
if (node.nodeType == 3 /* text node */ && node.nodeValue.match(/\S+/) /* non-empty */ ) {
365-
$compileNodes[index] = jqLite(node).wrap('<span></span>').parent()[0];
366+
$compileNodes[index] = node = jqLite(node).wrap('<span></span>').parent()[0];
366367
}
367368
});
368369
var compositeLinkFn = compileNodes($compileNodes, transcludeFn, $compileNodes, maxPriority);
@@ -420,7 +421,7 @@ function $CompileProvider($provide) {
420421
attrs = new Attributes();
421422

422423
// we must always refer to nodeList[i] since the nodes can be replaced underneath us.
423-
directives = collectDirectives(nodeList[i], [], attrs, maxPriority);
424+
directives = collectDirectives(nodeList[i], [], attrs, i == 0 ? maxPriority : undefined);
424425

425426
nodeLinkFn = (directives.length)
426427
? applyDirectivesToNode(directives, nodeList[i], attrs, transcludeFn, $rootElement)
@@ -509,6 +510,10 @@ function $CompileProvider($provide) {
509510
// iterate over the attributes
510511
for (var attr, name, nName, ngAttrName, value, nAttrs = node.attributes,
511512
j = 0, jj = nAttrs && nAttrs.length; j < jj; j++) {
513+
var attrStartName;
514+
var attrEndName;
515+
var index;
516+
512517
attr = nAttrs[j];
513518
if (attr.specified) {
514519
name = attr.name;
@@ -517,6 +522,11 @@ function $CompileProvider($provide) {
517522
if (NG_ATTR_BINDING.test(ngAttrName)) {
518523
name = ngAttrName.substr(6).toLowerCase();
519524
}
525+
if ((index = ngAttrName.lastIndexOf('Start')) != -1 && index == ngAttrName.length - 5) {
526+
attrStartName = name;
527+
attrEndName = name.substr(0, name.length - 5) + 'end';
528+
name = name.substr(0, name.length - 6);
529+
}
520530
nName = directiveNormalize(name.toLowerCase());
521531
attrsMap[nName] = name;
522532
attrs[nName] = value = trim((msie && name == 'href')
@@ -526,7 +536,7 @@ function $CompileProvider($provide) {
526536
attrs[nName] = true; // presence means true
527537
}
528538
addAttrInterpolateDirective(node, directives, value, nName);
529-
addDirective(directives, nName, 'A', maxPriority);
539+
addDirective(directives, nName, 'A', maxPriority, attrStartName, attrEndName);
530540
}
531541
}
532542

@@ -565,6 +575,47 @@ function $CompileProvider($provide) {
565575
return directives;
566576
}
567577

578+
/**
579+
* Given a node with an directive-start it collects all of the siblings until it find directive-end.
580+
* @param node
581+
* @param attrStart
582+
* @param attrEnd
583+
* @returns {*}
584+
*/
585+
function groupScan(node, attrStart, attrEnd) {
586+
var nodes = [];
587+
var depth = 0;
588+
if (attrStart && node.hasAttribute && node.hasAttribute(attrStart)) {
589+
var startNode = node;
590+
do {
591+
if (!node) {
592+
throw ngError(51, "Unterminated attribute, found '{0}' but no matching '{1}' found.", attrStart, attrEnd);
593+
}
594+
if (node.hasAttribute(attrStart)) depth++;
595+
if (node.hasAttribute(attrEnd)) depth--;
596+
nodes.push(node);
597+
node = node.nextSibling;
598+
} while (depth > 0);
599+
} else {
600+
nodes.push(node);
601+
}
602+
return jqLite(nodes);
603+
}
604+
605+
/**
606+
* Wrapper for linking function which converts normal linking function into a grouped
607+
* linking function.
608+
* @param linkFn
609+
* @param attrStart
610+
* @param attrEnd
611+
* @returns {Function}
612+
*/
613+
function groupElementsLinkFnWrapper(linkFn, attrStart, attrEnd) {
614+
return function(scope, element, attrs, controllers) {
615+
element = groupScan(element[0], attrStart, attrEnd);
616+
return linkFn(scope, element, attrs, controllers);
617+
}
618+
}
568619

569620
/**
570621
* Once the directives have been collected, their compile functions are executed. This method
@@ -601,6 +652,13 @@ function $CompileProvider($provide) {
601652
// executes all directives on the current element
602653
for(var i = 0, ii = directives.length; i < ii; i++) {
603654
directive = directives[i];
655+
var attrStart = directive.$$start;
656+
var attrEnd = directive.$$end;
657+
658+
// collect multiblock sections
659+
if (attrStart) {
660+
$compileNode = groupScan(compileNode, attrStart, attrEnd)
661+
}
604662
$template = undefined;
605663

606664
if (terminalPriority > directive.priority) {
@@ -631,11 +689,11 @@ function $CompileProvider($provide) {
631689
transcludeDirective = directive;
632690
terminalPriority = directive.priority;
633691
if (directiveValue == 'element') {
634-
$template = jqLite(compileNode);
692+
$template = groupScan(compileNode, attrStart, attrEnd)
635693
$compileNode = templateAttrs.$$element =
636694
jqLite(document.createComment(' ' + directiveName + ': ' + templateAttrs[directiveName] + ' '));
637695
compileNode = $compileNode[0];
638-
replaceWith(jqCollection, jqLite($template[0]), compileNode);
696+
replaceWith(jqCollection, jqLite(sliceArgs($template)), compileNode);
639697
childTranscludeFn = compile($template, transcludeFn, terminalPriority);
640698
} else {
641699
$template = jqLite(JQLiteClone(compileNode)).contents();
@@ -699,9 +757,9 @@ function $CompileProvider($provide) {
699757
try {
700758
linkFn = directive.compile($compileNode, templateAttrs, childTranscludeFn);
701759
if (isFunction(linkFn)) {
702-
addLinkFns(null, linkFn);
760+
addLinkFns(null, linkFn, attrStart, attrEnd);
703761
} else if (linkFn) {
704-
addLinkFns(linkFn.pre, linkFn.post);
762+
addLinkFns(linkFn.pre, linkFn.post, attrStart, attrEnd);
705763
}
706764
} catch (e) {
707765
$exceptionHandler(e, startingTag($compileNode));
@@ -723,12 +781,14 @@ function $CompileProvider($provide) {
723781

724782
////////////////////
725783

726-
function addLinkFns(pre, post) {
784+
function addLinkFns(pre, post, attrStart, attrEnd) {
727785
if (pre) {
786+
if (attrStart) pre = groupElementsLinkFnWrapper(pre, attrStart, attrEnd);
728787
pre.require = directive.require;
729788
preLinkFns.push(pre);
730789
}
731790
if (post) {
791+
if (attrStart) post = groupElementsLinkFnWrapper(post, attrStart, attrEnd);
732792
post.require = directive.require;
733793
postLinkFns.push(post);
734794
}
@@ -907,17 +967,20 @@ function $CompileProvider($provide) {
907967
* * `M`: comment
908968
* @returns true if directive was added.
909969
*/
910-
function addDirective(tDirectives, name, location, maxPriority) {
911-
var match = false;
970+
function addDirective(tDirectives, name, location, maxPriority, startAttrName, endAttrName) {
971+
var match = null;
912972
if (hasDirectives.hasOwnProperty(name)) {
913973
for(var directive, directives = $injector.get(name + Suffix),
914974
i = 0, ii = directives.length; i<ii; i++) {
915975
try {
916976
directive = directives[i];
917977
if ( (maxPriority === undefined || maxPriority > directive.priority) &&
918978
directive.restrict.indexOf(location) != -1) {
979+
if (startAttrName) {
980+
directive = inherit(directive, {$$start: startAttrName, $$end: endAttrName});
981+
}
919982
tDirectives.push(directive);
920-
match = true;
983+
match = directive;
921984
}
922985
} catch(e) { $exceptionHandler(e); }
923986
}
@@ -1120,30 +1183,50 @@ function $CompileProvider($provide) {
11201183
*
11211184
* @param {JqLite=} $rootElement The root of the compile tree. Used so that we can replace nodes
11221185
* in the root of the tree.
1123-
* @param {JqLite} $element The jqLite element which we are going to replace. We keep the shell,
1186+
* @param {JqLite} elementsToRemove The jqLite element which we are going to replace. We keep the shell,
11241187
* but replace its DOM node reference.
11251188
* @param {Node} newNode The new DOM node.
11261189
*/
1127-
function replaceWith($rootElement, $element, newNode) {
1128-
var oldNode = $element[0],
1129-
parent = oldNode.parentNode,
1190+
function replaceWith($rootElement, elementsToRemove, newNode) {
1191+
var firstElementToRemove = elementsToRemove[0],
1192+
removeCount = elementsToRemove.length,
1193+
parent = firstElementToRemove.parentNode,
11301194
i, ii;
11311195

11321196
if ($rootElement) {
11331197
for(i = 0, ii = $rootElement.length; i < ii; i++) {
1134-
if ($rootElement[i] == oldNode) {
1135-
$rootElement[i] = newNode;
1198+
if ($rootElement[i] == firstElementToRemove) {
1199+
$rootElement[i++] = newNode;
1200+
for (var j = i, j2 = j + removeCount - 1,
1201+
jj = $rootElement.length;
1202+
j < jj; j++, j2++) {
1203+
if (j2 < jj) {
1204+
$rootElement[j] = $rootElement[j2];
1205+
} else {
1206+
delete $rootElement[j];
1207+
}
1208+
}
1209+
$rootElement.length -= removeCount - 1;
11361210
break;
11371211
}
11381212
}
11391213
}
11401214

11411215
if (parent) {
1142-
parent.replaceChild(newNode, oldNode);
1216+
parent.replaceChild(newNode, firstElementToRemove);
1217+
}
1218+
var fragment = document.createDocumentFragment();
1219+
fragment.appendChild(firstElementToRemove);
1220+
newNode[jqLite.expando] = firstElementToRemove[jqLite.expando];
1221+
for (var k = 1, kk = elementsToRemove.length; k < kk; k++) {
1222+
var element = elementsToRemove[k];
1223+
jqLite(element).remove(); // must do this way to clean up expando
1224+
fragment.appendChild(element);
1225+
delete elementsToRemove[k];
11431226
}
11441227

1145-
newNode[jqLite.expando] = oldNode[jqLite.expando];
1146-
$element[0] = newNode;
1228+
elementsToRemove[0] = newNode;
1229+
elementsToRemove.length = 1
11471230
}
11481231
}];
11491232
}

src/ng/directive/ngRepeat.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -258,7 +258,7 @@ var ngRepeatDirective = ['$parse', '$animator', function($parse, $animator) {
258258
if (lastBlockMap.hasOwnProperty(key)) {
259259
block = lastBlockMap[key];
260260
animate.leave(block.element);
261-
block.element[0][NG_REMOVED] = true;
261+
forEach(block.element, function(element) { element[NG_REMOVED] = true});
262262
block.scope.$destroy();
263263
}
264264
}

test/jqLiteSpec.js

+8-4
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,9 @@ describe('jqLite', function() {
5656

5757
it('should allow construction with html', function() {
5858
var nodes = jqLite('<div>1</div><span>2</span>');
59+
expect(nodes[0].parentNode).toBeDefined();
60+
expect(nodes[0].parentNode.nodeType).toBe(11); /** Document Fragment **/;
61+
expect(nodes[0].parentNode).toBe(nodes[1].parentNode);
5962
expect(nodes.length).toEqual(2);
6063
expect(nodes[0].innerHTML).toEqual('1');
6164
expect(nodes[1].innerHTML).toEqual('2');
@@ -644,12 +647,13 @@ describe('jqLite', function() {
644647

645648

646649
it('should read/write value', function() {
647-
var element = jqLite('<div>abc</div>');
648-
expect(element.length).toEqual(1);
649-
expect(element[0].innerHTML).toEqual('abc');
650+
var element = jqLite('<div>ab</div><span>c</span>');
651+
expect(element.length).toEqual(2);
652+
expect(element[0].innerHTML).toEqual('ab');
653+
expect(element[1].innerHTML).toEqual('c');
650654
expect(element.text()).toEqual('abc');
651655
expect(element.text('xyz') == element).toBeTruthy();
652-
expect(element.text()).toEqual('xyz');
656+
expect(element.text()).toEqual('xyzxyz');
653657
});
654658
});
655659

0 commit comments

Comments
 (0)