mirror of
https://github.com/Hopiu/angular.js.git
synced 2026-03-22 09:30:28 +00:00
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>
This commit is contained in:
parent
b8ea7f6aba
commit
e46100f709
6 changed files with 276 additions and 51 deletions
|
|
@ -165,7 +165,8 @@ function JQLite(element) {
|
|||
div.innerHTML = '<div> </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;
|
||||
};
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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,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<ii; i++) {
|
||||
|
|
@ -916,8 +976,11 @@ function $CompileProvider($provide) {
|
|||
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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
'<div>' +
|
||||
'<span ng-show-start="show"></span>' +
|
||||
'<span ng-show-end></span>' +
|
||||
'</div>')($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(
|
||||
'<div>' +
|
||||
'<span ng-repeat-start="i in [1,2]">{{i}}A</span>' +
|
||||
'<span ng-repeat-end>{{i}}B;</span>' +
|
||||
'</div>')($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(
|
||||
'<div></div>' +
|
||||
'<span ng-repeat-start="i in [1,2]">{{i}}A</span>' +
|
||||
'<span ng-repeat-end>{{i}}B;</span>' +
|
||||
'<div></div>')($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(
|
||||
'<div></div>' +
|
||||
'<div ng-repeat-start="i in [1,2]">{{i}}A</div>' +
|
||||
'<span ng-bind-start="\'.\'"></span>' +
|
||||
'<span ng-bind-end></span>' +
|
||||
'<div ng-repeat-end>{{i}}B;</div>' +
|
||||
'<div></div>')($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(
|
||||
'<div></div>' +
|
||||
'<div ng-repeat-start="i in [1,2]">{{i}}(</div>' +
|
||||
'<span ng-repeat-start="j in [2,3]">{{j}}-</span>' +
|
||||
'<span ng-repeat-end>{{j}}</span>' +
|
||||
'<div ng-repeat-end>){{i}};</div>' +
|
||||
'<div></div>')($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(
|
||||
'<div>' +
|
||||
'<span foo-start></span>' +
|
||||
'</div>');
|
||||
}).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(
|
||||
'<div>' +
|
||||
'<span foo-start><span foo-end></span></span>' +
|
||||
'</div>');
|
||||
}).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(
|
||||
'<div>' +
|
||||
'<span data-ng-show-start="show"></span>' +
|
||||
'<span data-ng-show-end></span>' +
|
||||
'<span x-ng-show-start="show"></span>' +
|
||||
'<span x-ng-show-end></span>' +
|
||||
'</div>')($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');
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue