Skip to content

Commit fb875c0

Browse files
committed
fix($compile): abort compilation when duplicate element transclusion
Issue an error and abort compilation when two directives that ask for transclusion are found on a single element. This configuration is not supported and we previously failed to issue the error because in the case of element transclusion the compilation is re-started and this caused the compilation context to be lost. The ngRepeat directive has been special-cased to bypass this warning because it knows how to handle this scenario internally. This is not an ideal solution to the problem of multiple transclusions per element, we are hoping to have this configuration supported by the compiler in the future. See angular#4357. Closes angular#3893 Closes angular#4217 Closes angular#3307
1 parent 19a33cd commit fb875c0

File tree

3 files changed

+109
-24
lines changed

3 files changed

+109
-24
lines changed

src/ng/compile.js

+26-14
Original file line numberDiff line numberDiff line change
@@ -468,7 +468,7 @@ function $CompileProvider($provide) {
468468

469469
//================================
470470

471-
function compile($compileNodes, transcludeFn, maxPriority, ignoreDirective) {
471+
function compile($compileNodes, transcludeFn, maxPriority, ignoreDirective, previousCompileContext) {
472472
if (!($compileNodes instanceof jqLite)) {
473473
// jquery always rewraps, whereas we need to preserve the original selector so that we can modify it.
474474
$compileNodes = jqLite($compileNodes);
@@ -480,7 +480,7 @@ function $CompileProvider($provide) {
480480
$compileNodes[index] = node = jqLite(node).wrap('<span></span>').parent()[0];
481481
}
482482
});
483-
var compositeLinkFn = compileNodes($compileNodes, transcludeFn, $compileNodes, maxPriority, ignoreDirective);
483+
var compositeLinkFn = compileNodes($compileNodes, transcludeFn, $compileNodes, maxPriority, ignoreDirective, previousCompileContext);
484484
return function publicLinkFn(scope, cloneConnectFn){
485485
assertArg(scope, 'scope');
486486
// important!!: we must call our jqLite.clone() since the jQuery one is trying to be smart
@@ -527,7 +527,7 @@ function $CompileProvider($provide) {
527527
* @param {number=} max directive priority
528528
* @returns {?function} A composite linking function of all of the matched directives or null.
529529
*/
530-
function compileNodes(nodeList, transcludeFn, $rootElement, maxPriority, ignoreDirective) {
530+
function compileNodes(nodeList, transcludeFn, $rootElement, maxPriority, ignoreDirective, previousCompileContext) {
531531
var linkFns = [],
532532
nodeLinkFn, childLinkFn, directives, attrs, linkFnFound;
533533

@@ -538,7 +538,7 @@ function $CompileProvider($provide) {
538538
directives = collectDirectives(nodeList[i], [], attrs, i == 0 ? maxPriority : undefined, ignoreDirective);
539539

540540
nodeLinkFn = (directives.length)
541-
? applyDirectivesToNode(directives, nodeList[i], attrs, transcludeFn, $rootElement, null, [], [])
541+
? applyDirectivesToNode(directives, nodeList[i], attrs, transcludeFn, $rootElement, null, [], [], previousCompileContext)
542542
: null;
543543

544544
childLinkFn = (nodeLinkFn && nodeLinkFn.terminal || !nodeList[i].childNodes || !nodeList[i].childNodes.length)
@@ -549,6 +549,7 @@ function $CompileProvider($provide) {
549549
linkFns.push(nodeLinkFn);
550550
linkFns.push(childLinkFn);
551551
linkFnFound = (linkFnFound || nodeLinkFn || childLinkFn);
552+
previousCompileContext = null; //use the previous context only for the first element in the virtual group
552553
}
553554

554555
// return a linking function if we have found anything, null otherwise
@@ -749,23 +750,26 @@ function $CompileProvider($provide) {
749750
* scope argument is auto-generated to the new child of the transcluded parent scope.
750751
* @param {JQLite} jqCollection If we are working on the root of the compile tree then this
751752
* argument has the root jqLite array so that we can replace nodes on it.
752-
* @param {Object=} ignoreDirective An optional directive that will be ignored when compiling
753+
* @param {Object=} originalReplaceDirective An optional directive that will be ignored when compiling
753754
* the transclusion.
754755
* @param {Array.<Function>} preLinkFns
755756
* @param {Array.<Function>} postLinkFns
757+
* @param {Object} previousCompileContext Context used for previous compilation of the current node
756758
* @returns linkFn
757759
*/
758760
function applyDirectivesToNode(directives, compileNode, templateAttrs, transcludeFn, jqCollection,
759-
originalReplaceDirective, preLinkFns, postLinkFns) {
761+
originalReplaceDirective, preLinkFns, postLinkFns, previousCompileContext) {
762+
previousCompileContext = previousCompileContext || {};
763+
760764
var terminalPriority = -Number.MAX_VALUE,
761-
newScopeDirective = null,
762-
newIsolateScopeDirective = null,
763-
templateDirective = null,
765+
newScopeDirective,
766+
newIsolateScopeDirective = previousCompileContext.newIsolateScopeDirective,
767+
templateDirective = previousCompileContext.templateDirective,
764768
$compileNode = templateAttrs.$$element = jqLite(compileNode),
765769
directive,
766770
directiveName,
767771
$template,
768-
transcludeDirective,
772+
transcludeDirective = previousCompileContext.transcludeDirective,
769773
replaceDirective = originalReplaceDirective,
770774
childTranscludeFn = transcludeFn,
771775
controllerDirectives,
@@ -814,19 +818,27 @@ function $CompileProvider($provide) {
814818
}
815819

816820
if (directiveValue = directive.transclude) {
817-
assertNoDuplicate('transclusion', transcludeDirective, directive, $compileNode);
818-
transcludeDirective = directive;
821+
// Special case ngRepeat so that we don't complain about duplicate transclusion, ngRepeat knows how to handle
822+
// this on its own.
823+
if (directiveName !== 'ngRepeat') {
824+
assertNoDuplicate('transclusion', transcludeDirective, directive, $compileNode);
825+
transcludeDirective = directive;
826+
}
819827

820828
if (directiveValue == 'element') {
821829
terminalPriority = directive.priority;
822-
$template = groupScan(compileNode, attrStart, attrEnd)
830+
$template = groupScan(compileNode, attrStart, attrEnd);
823831
$compileNode = templateAttrs.$$element =
824832
jqLite(document.createComment(' ' + directiveName + ': ' + templateAttrs[directiveName] + ' '));
825833
compileNode = $compileNode[0];
826834
replaceWith(jqCollection, jqLite(sliceArgs($template)), compileNode);
827835

828836
childTranscludeFn = compile($template, transcludeFn, terminalPriority,
829-
replaceDirective && replaceDirective.name);
837+
replaceDirective && replaceDirective.name, {
838+
newIsolateScopeDirective: newIsolateScopeDirective,
839+
transcludeDirective: transcludeDirective,
840+
templateDirective: templateDirective
841+
});
830842
} else {
831843
$template = jqLite(JQLiteClone(compileNode)).contents();
832844
$compileNode.html(''); // clear contents

test/ng/compileSpec.js

+46-10
Original file line numberDiff line numberDiff line change
@@ -1205,7 +1205,7 @@ describe('$compile', function() {
12051205
));
12061206

12071207

1208-
it('should work when directive is a repeater', inject(
1208+
it('should work when directive is in a repeater', inject(
12091209
function($compile, $httpBackend, $rootScope) {
12101210
$httpBackend.expect('GET', 'hello.html').
12111211
respond('<span>i=<span ng-transclude></span>;</span>');
@@ -1317,7 +1317,7 @@ describe('$compile', function() {
13171317
});
13181318

13191319

1320-
describe('template as function', function() {
1320+
describe('templateUrl as function', function() {
13211321

13221322
beforeEach(module(function() {
13231323
directive('myDirective', valueFn({
@@ -2745,23 +2745,59 @@ describe('$compile', function() {
27452745
});
27462746

27472747

2748-
it('should only allow one transclude per element', function() {
2748+
it('should only allow one content transclusion per element', function() {
2749+
module(function() {
2750+
directive('first', valueFn({
2751+
transclude: true
2752+
}));
2753+
directive('second', valueFn({
2754+
transclude: true
2755+
}));
2756+
});
2757+
inject(function($compile) {
2758+
expect(function() {
2759+
$compile('<div first="" second=""></div>');
2760+
}).toThrowMinErr('$compile', 'multidir', 'Multiple directives [first, second] asking for transclusion on: ' +
2761+
'<div first="" second="">');
2762+
});
2763+
});
2764+
2765+
2766+
it('should only allow one element transclusion per element', function() {
27492767
module(function() {
27502768
directive('first', valueFn({
2751-
scope: {},
2752-
restrict: 'CA',
2753-
transclude: 'content'
2769+
transclude: 'element'
2770+
}));
2771+
directive('second', valueFn({
2772+
transclude: 'element'
2773+
}));
2774+
});
2775+
inject(function($compile) {
2776+
expect(function() {
2777+
$compile('<div first second></div>');
2778+
}).toThrowMinErr('$compile', 'multidir', 'Multiple directives [first, second] asking for transclusion on: ' +
2779+
'<!-- first: -->');
2780+
});
2781+
});
2782+
2783+
2784+
it('should only allow one element transclusion per element when directives have different priorities', function() {
2785+
// we restart compilation in this case and we need to remember the duplicates during the second compile
2786+
// regression #3893
2787+
module(function() {
2788+
directive('first', valueFn({
2789+
transclude: 'element',
2790+
priority: 100
27542791
}));
27552792
directive('second', valueFn({
2756-
restrict: 'CA',
2757-
transclude: 'content'
2793+
transclude: 'element'
27582794
}));
27592795
});
27602796
inject(function($compile) {
27612797
expect(function() {
2762-
$compile('<div class="first second"></div>');
2798+
$compile('<div first second></div>');
27632799
}).toThrowMinErr('$compile', 'multidir', 'Multiple directives [first, second] asking for transclusion on: ' +
2764-
'<div class="first second ng-isolate-scope ng-scope">');
2800+
'<div first="" second="">');
27652801
});
27662802
});
27672803

test/ng/directive/ngRepeatSpec.js

+37
Original file line numberDiff line numberDiff line change
@@ -976,6 +976,43 @@ describe('ngRepeat', function() {
976976
});
977977

978978

979+
describe('compatibility', function() {
980+
981+
it('should allow mixing ngRepeat and another element transclusion directive', function() {
982+
$compileProvider.directive('elmTrans', valueFn({
983+
transclude: 'element',
984+
controller: function($transclude, $scope, $element) {
985+
$transclude(function(transcludedNodes) {
986+
$element.after(']]').after(transcludedNodes).after('[[');
987+
});
988+
}
989+
}));
990+
991+
inject(function($compile, $rootScope) {
992+
element = $compile('<div><div ng-repeat="i in [1,2]" elm-trans>{{i}}</div></div>')($rootScope);
993+
$rootScope.$digest();
994+
expect(element.text()).toBe('[[1]][[2]]')
995+
});
996+
});
997+
998+
999+
it('should allow mixing ngRepeat with ngInclude', inject(function($compile, $rootScope, $httpBackend) {
1000+
$httpBackend.whenGET('someTemplate.html').respond('<p>some template; </p>');
1001+
element = $compile('<div><div ng-repeat="i in [1,2]" ng-include="\'someTemplate.html\'"></div></div>')($rootScope);
1002+
$rootScope.$digest();
1003+
$httpBackend.flush();
1004+
expect(element.text()).toBe('some template; some template; ');
1005+
}));
1006+
1007+
1008+
it('should allow mixing ngRepeat with ngIf', inject(function($compile, $rootScope) {
1009+
element = $compile('<div><div ng-repeat="i in [1,2,3,4]" ng-if="i % 2 == 0">{{i}};</div></div>')($rootScope);
1010+
$rootScope.$digest();
1011+
expect(element.text()).toBe('2;4;');
1012+
}));
1013+
});
1014+
1015+
9791016
describe('ngRepeatStart', function () {
9801017
it('should grow multi-node repeater', inject(function($compile, $rootScope) {
9811018
$rootScope.show = false;

0 commit comments

Comments
 (0)