Skip to content

Commit 1d3fce3

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 7c1dbc5 commit 1d3fce3

File tree

3 files changed

+142
-29
lines changed

3 files changed

+142
-29
lines changed

src/ng/compile.js

+33-17
Original file line numberDiff line numberDiff line change
@@ -469,7 +469,7 @@ function $CompileProvider($provide) {
469469

470470
//================================
471471

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

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

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

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

555556
// return a linking function if we have found anything, null otherwise
@@ -750,23 +751,26 @@ function $CompileProvider($provide) {
750751
* scope argument is auto-generated to the new child of the transcluded parent scope.
751752
* @param {JQLite} jqCollection If we are working on the root of the compile tree then this
752753
* argument has the root jqLite array so that we can replace nodes on it.
753-
* @param {Object=} ignoreDirective An optional directive that will be ignored when compiling
754+
* @param {Object=} originalReplaceDirective An optional directive that will be ignored when compiling
754755
* the transclusion.
755756
* @param {Array.<Function>} preLinkFns
756757
* @param {Array.<Function>} postLinkFns
758+
* @param {Object} previousCompileContext Context used for previous compilation of the current node
757759
* @returns linkFn
758760
*/
759761
function applyDirectivesToNode(directives, compileNode, templateAttrs, transcludeFn, jqCollection,
760-
originalReplaceDirective, preLinkFns, postLinkFns) {
762+
originalReplaceDirective, preLinkFns, postLinkFns, previousCompileContext) {
763+
previousCompileContext = previousCompileContext || {};
764+
761765
var terminalPriority = -Number.MAX_VALUE,
762-
newScopeDirective = null,
763-
newIsolateScopeDirective = null,
764-
templateDirective = null,
766+
newScopeDirective,
767+
newIsolateScopeDirective = previousCompileContext.newIsolateScopeDirective,
768+
templateDirective = previousCompileContext.templateDirective,
765769
$compileNode = templateAttrs.$$element = jqLite(compileNode),
766770
directive,
767771
directiveName,
768772
$template,
769-
transcludeDirective,
773+
transcludeDirective = previousCompileContext.transcludeDirective,
770774
replaceDirective = originalReplaceDirective,
771775
childTranscludeFn = transcludeFn,
772776
controllerDirectives,
@@ -815,19 +819,27 @@ function $CompileProvider($provide) {
815819
}
816820

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

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

829837
childTranscludeFn = compile($template, transcludeFn, terminalPriority,
830-
replaceDirective && replaceDirective.name);
838+
replaceDirective && replaceDirective.name, {
839+
newIsolateScopeDirective: newIsolateScopeDirective,
840+
transcludeDirective: transcludeDirective,
841+
templateDirective: templateDirective
842+
});
831843
} else {
832844
$template = jqLite(JQLiteClone(compileNode)).contents();
833845
$compileNode.html(''); // clear contents
@@ -889,7 +901,11 @@ function $CompileProvider($provide) {
889901
}
890902

891903
nodeLinkFn = compileTemplateUrl(directives.splice(i, directives.length - i), $compileNode,
892-
templateAttrs, jqCollection, childTranscludeFn, preLinkFns, postLinkFns);
904+
templateAttrs, jqCollection, childTranscludeFn, preLinkFns, postLinkFns, {
905+
newIsolateScopeDirective: newIsolateScopeDirective,
906+
transcludeDirective: transcludeDirective,
907+
templateDirective: templateDirective
908+
});
893909
ii = directives.length;
894910
} else if (directive.compile) {
895911
try {
@@ -1188,7 +1204,7 @@ function $CompileProvider($provide) {
11881204

11891205

11901206
function compileTemplateUrl(directives, $compileNode, tAttrs,
1191-
$rootElement, childTranscludeFn, preLinkFns, postLinkFns) {
1207+
$rootElement, childTranscludeFn, preLinkFns, postLinkFns, previousCompileContext) {
11921208
var linkQueue = [],
11931209
afterTemplateNodeLinkFn,
11941210
afterTemplateChildLinkFn,
@@ -1231,7 +1247,7 @@ function $CompileProvider($provide) {
12311247
directives.unshift(derivedSyncDirective);
12321248

12331249
afterTemplateNodeLinkFn = applyDirectivesToNode(directives, compileNode, tAttrs,
1234-
childTranscludeFn, $compileNode, origAsyncDirective, preLinkFns, postLinkFns);
1250+
childTranscludeFn, $compileNode, origAsyncDirective, preLinkFns, postLinkFns, previousCompileContext);
12351251
forEach($rootElement, function(node, i) {
12361252
if (node == compileNode) {
12371253
$rootElement[i] = $compileNode[0];

test/ng/compileSpec.js

+72-12
Original file line numberDiff line numberDiff line change
@@ -1042,10 +1042,12 @@ describe('$compile', function() {
10421042
templateUrl: 'template.html'
10431043
}));
10441044
});
1045-
inject(function($compile){
1045+
inject(function($compile, $httpBackend){
1046+
$httpBackend.whenGET('template.html').respond('<p>template.html</p>');
10461047
expect(function() {
10471048
$compile('<div><div class="sync async"></div></div>');
1048-
}).toThrowMinErr('$compile', 'multidir', 'Multiple directives [sync, async] asking for template on: '+
1049+
$httpBackend.flush();
1050+
}).toThrowMinErr('$compile', 'multidir', 'Multiple directives [async, sync] asking for template on: '+
10491051
'<div class="sync async">');
10501052
});
10511053
});
@@ -1205,7 +1207,7 @@ describe('$compile', function() {
12051207
));
12061208

12071209

1208-
it('should work when directive is a repeater', inject(
1210+
it('should work when directive is in a repeater', inject(
12091211
function($compile, $httpBackend, $rootScope) {
12101212
$httpBackend.expect('GET', 'hello.html').
12111213
respond('<span>i=<span ng-transclude></span>;</span>');
@@ -1317,7 +1319,7 @@ describe('$compile', function() {
13171319
});
13181320

13191321

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

13221324
beforeEach(module(function() {
13231325
directive('myDirective', valueFn({
@@ -2745,23 +2747,81 @@ describe('$compile', function() {
27452747
});
27462748

27472749

2748-
it('should only allow one transclude per element', function() {
2750+
it('should only allow one content transclusion per element', function() {
27492751
module(function() {
27502752
directive('first', valueFn({
2751-
scope: {},
2752-
restrict: 'CA',
2753-
transclude: 'content'
2753+
transclude: true
27542754
}));
27552755
directive('second', valueFn({
2756-
restrict: 'CA',
2757-
transclude: 'content'
2756+
transclude: true
27582757
}));
27592758
});
27602759
inject(function($compile) {
27612760
expect(function() {
2762-
$compile('<div class="first second"></div>');
2761+
$compile('<div first="" second=""></div>');
2762+
}).toThrowMinErr('$compile', 'multidir', /Multiple directives \[first, second\] asking for transclusion on: <div .+/);
2763+
});
2764+
});
2765+
2766+
2767+
it('should only allow one element transclusion per element', function() {
2768+
module(function() {
2769+
directive('first', valueFn({
2770+
transclude: 'element'
2771+
}));
2772+
directive('second', valueFn({
2773+
transclude: 'element'
2774+
}));
2775+
});
2776+
inject(function($compile) {
2777+
expect(function() {
2778+
$compile('<div first second></div>');
27632779
}).toThrowMinErr('$compile', 'multidir', 'Multiple directives [first, second] asking for transclusion on: ' +
2764-
'<div class="first second ng-isolate-scope ng-scope">');
2780+
'<!-- first: -->');
2781+
});
2782+
});
2783+
2784+
2785+
it('should only allow one element transclusion per element when directives have different priorities', function() {
2786+
// we restart compilation in this case and we need to remember the duplicates during the second compile
2787+
// regression #3893
2788+
module(function() {
2789+
directive('first', valueFn({
2790+
transclude: 'element',
2791+
priority: 100
2792+
}));
2793+
directive('second', valueFn({
2794+
transclude: 'element'
2795+
}));
2796+
});
2797+
inject(function($compile) {
2798+
expect(function() {
2799+
$compile('<div first second></div>');
2800+
}).toThrowMinErr('$compile', 'multidir', /Multiple directives \[first, second\] asking for transclusion on: <div .+/);
2801+
});
2802+
});
2803+
2804+
2805+
it('should only allow one element transclusion per element when async replace directive is in the mix', function() {
2806+
module(function() {
2807+
directive('template', valueFn({
2808+
templateUrl: 'template.html',
2809+
replace: true
2810+
}));
2811+
directive('first', valueFn({
2812+
transclude: 'element',
2813+
priority: 100
2814+
}));
2815+
directive('second', valueFn({
2816+
transclude: 'element'
2817+
}));
2818+
});
2819+
inject(function($compile, $httpBackend) {
2820+
$httpBackend.expectGET('template.html').respond('<p second>template.html</p>');
2821+
$compile('<div template first></div>');
2822+
expect(function() {
2823+
$httpBackend.flush();
2824+
}).toThrowMinErr('$compile', 'multidir', /Multiple directives \[first, second\] asking for transclusion on: <p .+/);
27652825
});
27662826
});
27672827

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)