Skip to content

Commit 130fd03

Browse files
committed
refactor(compile): Optimizations for the directive 'multi-element'
Avoid cloning elements during a multi-element transclude Put only one comment that will be sent to the linking function and remove all the placeholde elements Cleanup the containsAttr method with an implementation from @petebacondarwin Fixes on the unit tests to follow angular#2492 Document the transclude type 'multi-element' at the docs Fix a typo on the method compileNodes Move the important unit tests from ngRepeatStartSpec to ngRepeatSpec Add more tests at compile for the new multi-element transclude
1 parent efd5bd7 commit 130fd03

File tree

5 files changed

+278
-895
lines changed

5 files changed

+278
-895
lines changed

docs/content/guide/directive.ngdoc

+5-1
Original file line numberDiff line numberDiff line change
@@ -444,7 +444,11 @@ compiler}. The attributes are:
444444

445445
* `true` - transclude the content of the directive.
446446
* `'element'` - transclude the whole element including any directives defined at lower priority.
447-
447+
* `'multi-element'` - if the node where this directive is present contains an attribute
448+
named `${directiveName}-start` (were `${directiveName}` is the name of the directive) then
449+
the entire block of elements is transcluded until a sibling node is found with and attribute
450+
named `${directiveName}-end`. If and attribute name `${directiveName}-start` is not found
451+
then it behaves like the `'element'` transclude.
448452

449453
* `compile`: This is the compile function described in the section below.
450454

src/ng/compile.js

+31-32
Original file line numberDiff line numberDiff line change
@@ -419,9 +419,10 @@ function $CompileProvider($provide) {
419419
* rootElement must be set the jqLite collection of the compile root. This is
420420
* needed so that the jqLite collection items can be replaced with widgets.
421421
* @param {number=} max directive priority
422-
* @param {boolean=} if the max priority should only apply to the first element in the list.
423-
* A true value here will make the maxPriority only apply to the first element on the
424-
* list while the other elements on the list will not have a maxPriority set
422+
* @param {boolean=} limitMaxPriorityToFirstElement if the max priority should only apply to
423+
* the first element in the list. A true value here will make the maxPriority only apply
424+
* to the first element on the list while the other elements on the list will not have
425+
* a maxPriority set
425426
* @returns {?function} A composite linking function of all of the matched directives or null.
426427
*/
427428
function compileNodes(nodeList, transcludeFn, $rootElement, maxPriority, limitMaxPriorityToFirstElement) {
@@ -654,20 +655,20 @@ function $CompileProvider($provide) {
654655
// We need to compile a clone of the elements, but at the same time we have to be sure that these elements are siblings
655656
var nestedContent = extractMultiElementTransclude(compileNode, directiveName);
656657
var cloneContent = jqLite('<div></div>');
657-
forEach(nestedContent, function(nestedElement) {
658-
cloneContent.append(JQLiteClone(nestedElement));
659-
});
660-
childTranscludeFn = compile(cloneContent.contents(), transcludeFn, terminalPriority, true);
658+
661659
$template = jqLite(compileNode);
662660
$compileNode = templateAttrs.$$element =
663661
jqLite(document.createComment(' ' + directiveName + ': ' + templateAttrs[directiveName] + ' '));
664662
compileNode = $compileNode[0];
665-
replaceWith($rootElement, $template, compileNode);
663+
replaceWith($rootElement, jqLite($template[0]), compileNode);
664+
cloneContent.append($template);
666665
forEach(nestedContent.splice(1),
667666
function(toRemove) {
668-
replaceWith($rootElement, jqLite(toRemove), document.createComment(' placeholder '));
667+
removeElement($rootElement, toRemove);
668+
cloneContent.append(toRemove);
669669
}
670670
);
671+
childTranscludeFn = compile(cloneContent.contents(), transcludeFn, terminalPriority, true);
671672
} else {
672673
$template = jqLite(JQLiteClone(compileNode)).contents();
673674
$compileNode.html(''); // clear contents
@@ -774,32 +775,16 @@ function $CompileProvider($provide) {
774775

775776

776777
function containsAttr(node, attributeName) {
777-
var nodeType = node.nodeType,
778-
result = false;
779-
780-
switch(nodeType) {
781-
case 1:
782-
// iterate over the attributes
783-
for (var attr, name, nName, ngAttrName, nAttrs = node.attributes,
784-
j = 0, jj = nAttrs && nAttrs.length; j < jj; j++) {
785-
attr = nAttrs[j];
786-
if (attr.specified) {
787-
name = attr.name;
788-
// support ngAttr attribute binding
789-
ngAttrName = directiveNormalize(name);
790-
if (NG_ATTR_BINDING.test(ngAttrName)) {
791-
name = ngAttrName.substr(6).toLowerCase();
792-
}
793-
nName = directiveNormalize(name.toLowerCase());
794-
if (nName == attributeName) {
795-
result = true;
796-
break;
797-
}
778+
var attr, attrs = node.attributes, length = attrs && attrs.length;
779+
if ( length ) {
780+
for (var j = 0; j < length; j++) {
781+
attr = attrs[j];
782+
if (attr.specified && directiveNormalize(attr.name) == attributeName) {
783+
return true;
798784
}
799785
}
800-
break;
801786
}
802-
return result;
787+
return false;
803788
}
804789

805790

@@ -1224,6 +1209,20 @@ function $CompileProvider($provide) {
12241209
newNode[jqLite.expando] = oldNode[jqLite.expando];
12251210
$element[0] = newNode;
12261211
}
1212+
1213+
1214+
function removeElement($rootElement, element) {
1215+
var i, ii;
1216+
1217+
if ($rootElement) {
1218+
for(i = 0, ii = $rootElement.length; i < ii; i++) {
1219+
if ($rootElement[i] == element) {
1220+
$rootElement.splice(i, 1);
1221+
break;
1222+
}
1223+
}
1224+
}
1225+
}
12271226
}];
12281227
}
12291228

test/ng/compileSpec.js

+94
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,47 @@ describe('$compile', function() {
381381
})
382382
});
383383

384+
it("should complain when the multi-element end tag can't be found among one of the following siblings", inject(function($compile) {
385+
forEach([
386+
// no content, no end tag
387+
'<div ng-repeat="item in items" ng-repeat-start></div>',
388+
389+
// content, no end tag
390+
'<div ng-repeat="item in items" ng-repeat-start></div>' +
391+
'<span>{{item.text}}></span>' +
392+
'<span>{{item.done}}</span>',
393+
394+
// content, end tag too deep
395+
'<div ng-repeat="item in items" ng-repeat-start></div>' +
396+
'<div>' +
397+
'<span>{{item.text}}></span>' +
398+
'<span>{{item.done}}</span>' +
399+
'<span ng-repeat-end></span>' +
400+
'</div>',
401+
402+
// content, end tag too high
403+
'<div>' +
404+
'<div ng-repeat="item in items" ng-repeat-start></div>' +
405+
'<span>{{item.text}}></span>' +
406+
'<span>{{item.done}}</span>' +
407+
'</div>' +
408+
'<div ng-repeat-end></div>',
409+
410+
], function(template) {
411+
expect(function() {
412+
$compile('<div>' + template + '</div>');
413+
}).toThrow("Unmatched ngRepeatStart.");
414+
});
415+
}));
416+
417+
418+
it("should complain when there is an end attribute at the start of a multi-element directive", inject(function($compile) {
419+
expect(function() {
420+
$compile('<div><li ng-repeat-end ng-repeat="i in items"></li><li></li></div>');
421+
}).toThrow("Unexpected ngRepeatEnd.");
422+
}));
423+
424+
384425
describe('compiler control', function() {
385426
describe('priority', function() {
386427
it('should honor priority', inject(function($compile, $rootScope, log){
@@ -2345,6 +2386,35 @@ describe('$compile', function() {
23452386
});
23462387

23472388

2389+
it('should compile get templateFn on multi-element', function() {
2390+
module(function() {
2391+
directive('trans', function(log) {
2392+
return {
2393+
transclude: 'multi-element',
2394+
priority: 2,
2395+
controller: function($transclude) { this.$transclude = $transclude; },
2396+
compile: function(element, attrs, template) {
2397+
log('compile: ' + angular.mock.dump(element));
2398+
return function(scope, element, attrs, ctrl) {
2399+
log('link');
2400+
var cursor = element;
2401+
template(scope.$new(), function(clone) {cursor.after(clone); cursor = clone.eq(-1);});
2402+
ctrl.$transclude(function(clone) {cursor.after(clone)});
2403+
};
2404+
}
2405+
}
2406+
});
2407+
});
2408+
inject(function(log, $rootScope, $compile) {
2409+
element = $compile('<div><div high-log trans="text" trans-start log>{{$parent.$id}}-{{$id}};</div><div trans-end>{{$parent.$id}}-{{$id}};</div></div>')
2410+
($rootScope);
2411+
$rootScope.$apply();
2412+
expect(log).toEqual('compile: <!-- trans: text -->; HIGH; link; LOG; LOG');
2413+
expect(element.text()).toEqual('001-002;001-002;001-003;001-003;');
2414+
});
2415+
});
2416+
2417+
23482418
it('should support transclude directive', function() {
23492419
module(function() {
23502420
directive('trans', function() {
@@ -2485,6 +2555,30 @@ describe('$compile', function() {
24852555
});
24862556

24872557

2558+
it('should support transcluded multi-element on root content', function() {
2559+
var comment;
2560+
module(function() {
2561+
directive('transclude', valueFn({
2562+
transclude: 'multi-element',
2563+
compile: function(element, attr, linker) {
2564+
return function(scope, element, attr) {
2565+
comment = element;
2566+
};
2567+
}
2568+
}));
2569+
});
2570+
inject(function($compile, $rootScope) {
2571+
var element = jqLite('<div>before<div transclude transclude-begin></div><div transclude-end></div>after</div>').contents();
2572+
expect(element.length).toEqual(4);
2573+
expect(nodeName_(element[1])).toBe('DIV');
2574+
$compile(element)($rootScope);
2575+
expect(nodeName_(element[1])).toBe('#comment');
2576+
expect(nodeName_(comment)).toBe('#comment');
2577+
expect(nodeName_(element[2])).toBe('DIV');
2578+
});
2579+
});
2580+
2581+
24882582
it('should safely create transclude comment node and not break with "-->"',
24892583
inject(function($rootScope) {
24902584
// see: https://github.com/angular/angular.js/issues/1740

0 commit comments

Comments
 (0)