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

Commit

Permalink
feat($compile): support multi-element directive
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
mhevery committed May 29, 2013
1 parent b8ea7f6 commit e46100f
Show file tree
Hide file tree
Showing 6 changed files with 276 additions and 51 deletions.
50 changes: 29 additions & 21 deletions src/jqLite.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down Expand Up @@ -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)) {
Expand Down Expand Up @@ -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
Expand All @@ -529,7 +538,6 @@ forEach({
// return self for chaining
return this;
}
return fn.$dv;
};
});

Expand Down
15 changes: 10 additions & 5 deletions src/ng/animator.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
123 changes: 103 additions & 20 deletions src/ng/compile.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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;
Expand All @@ -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')
Expand All @@ -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);
}
}

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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));
Expand All @@ -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);
}
Expand Down Expand Up @@ -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); }
}
Expand Down Expand Up @@ -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
}
}];
}
Expand Down
2 changes: 1 addition & 1 deletion src/ng/directive/ngRepeat.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
}
Expand Down
12 changes: 8 additions & 4 deletions test/jqLiteSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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');
});
});

Expand Down
Loading

7 comments on commit e46100f

@VonD
Copy link

@VonD VonD commented on e46100f Jun 10, 2013

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

@grettke
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

@markrendle
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

@Leeiio
Copy link

@Leeiio Leeiio commented on e46100f Jul 23, 2013

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can not wait to use it!

@fizx
Copy link

@fizx fizx commented on e46100f Aug 13, 2013

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

@burkeholland
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

@caitp
Copy link
Contributor

@caitp caitp commented on e46100f Feb 17, 2014

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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.