diff --git a/src/ng/compile.js b/src/ng/compile.js index e783307615cf..f7ca7af38585 100644 --- a/src/ng/compile.js +++ b/src/ng/compile.js @@ -830,7 +830,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { compileNodes($compileNodes, transcludeFn, $compileNodes, maxPriority, ignoreDirective, previousCompileContext); safeAddClass($compileNodes, 'ng-scope'); - return function publicLinkFn(scope, cloneConnectFn, transcludeControllers){ + return function publicLinkFn(scope, cloneConnectFn, transcludeControllers, parentBoundTranscludeFn){ assertArg(scope, 'scope'); // important!!: we must call our jqLite.clone() since the jQuery one is trying to be smart // and sometimes changes the structure of the DOM. @@ -852,7 +852,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { } if (cloneConnectFn) cloneConnectFn($linkNode, scope); - if (compositeLinkFn) compositeLinkFn(scope, $linkNode, $linkNode); + if (compositeLinkFn) compositeLinkFn(scope, $linkNode, $linkNode, parentBoundTranscludeFn); return $linkNode; }; } @@ -907,7 +907,9 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { !childNodes.length) ? null : compileNodes(childNodes, - nodeLinkFn ? nodeLinkFn.transclude : transcludeFn); + nodeLinkFn ? ( + (nodeLinkFn.transcludeOnThisElement || !nodeLinkFn.templateOnThisElement) + && nodeLinkFn.transclude) : transcludeFn); linkFns.push(nodeLinkFn, childLinkFn); linkFnFound = linkFnFound || nodeLinkFn || childLinkFn; @@ -918,8 +920,8 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { // return a linking function if we have found anything, null otherwise return linkFnFound ? compositeLinkFn : null; - function compositeLinkFn(scope, nodeList, $rootElement, boundTranscludeFn) { - var nodeLinkFn, childLinkFn, node, $node, childScope, childTranscludeFn, i, ii, n; + function compositeLinkFn(scope, nodeList, $rootElement, parentBoundTranscludeFn) { + var nodeLinkFn, childLinkFn, node, $node, childScope, i, ii, n, childBoundTranscludeFn; // copy nodeList so that linking doesn't break due to live list updates. var nodeListLength = nodeList.length, @@ -941,23 +943,32 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { } else { childScope = scope; } - childTranscludeFn = nodeLinkFn.transclude; - if (childTranscludeFn || (!boundTranscludeFn && transcludeFn)) { - nodeLinkFn(childLinkFn, childScope, node, $rootElement, - createBoundTranscludeFn(scope, childTranscludeFn || transcludeFn) - ); + + if ( nodeLinkFn.transcludeOnThisElement ) { + childBoundTranscludeFn = createBoundTranscludeFn(scope, nodeLinkFn.transclude, parentBoundTranscludeFn); + + } else if (!nodeLinkFn.templateOnThisElement && parentBoundTranscludeFn) { + childBoundTranscludeFn = parentBoundTranscludeFn; + + } else if (!parentBoundTranscludeFn && transcludeFn) { + childBoundTranscludeFn = createBoundTranscludeFn(scope, transcludeFn); + } else { - nodeLinkFn(childLinkFn, childScope, node, $rootElement, boundTranscludeFn); + childBoundTranscludeFn = null; } + + nodeLinkFn(childLinkFn, childScope, node, $rootElement, childBoundTranscludeFn); + } else if (childLinkFn) { - childLinkFn(scope, node.childNodes, undefined, boundTranscludeFn); + childLinkFn(scope, node.childNodes, undefined, parentBoundTranscludeFn); } } } } - function createBoundTranscludeFn(scope, transcludeFn) { - return function boundTranscludeFn(transcludedScope, cloneFn, controllers) { + function createBoundTranscludeFn(scope, transcludeFn, previousBoundTranscludeFn) { + + var boundTranscludeFn = function(transcludedScope, cloneFn, controllers) { var scopeCreated = false; if (!transcludedScope) { @@ -966,12 +977,14 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { scopeCreated = true; } - var clone = transcludeFn(transcludedScope, cloneFn, controllers); + var clone = transcludeFn(transcludedScope, cloneFn, controllers, previousBoundTranscludeFn); if (scopeCreated) { - clone.on('$destroy', bind(transcludedScope, transcludedScope.$destroy)); + clone.on('$destroy', function() { transcludedScope.$destroy(); }); } return clone; }; + + return boundTranscludeFn; } /** @@ -1149,6 +1162,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { templateDirective = previousCompileContext.templateDirective, nonTlbTranscludeDirective = previousCompileContext.nonTlbTranscludeDirective, hasTranscludeDirective = false, + hasTemplate = false, hasElementTranscludeDirective = previousCompileContext.hasElementTranscludeDirective, $compileNode = templateAttrs.$$element = jqLite(compileNode), directive, @@ -1239,6 +1253,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { } if (directive.template) { + hasTemplate = true; assertNoDuplicate('template', templateDirective, directive, $compileNode); templateDirective = directive; @@ -1288,6 +1303,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { } if (directive.templateUrl) { + hasTemplate = true; assertNoDuplicate('template', templateDirective, directive, $compileNode); templateDirective = directive; @@ -1296,7 +1312,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { } nodeLinkFn = compileTemplateUrl(directives.splice(i, directives.length - i), $compileNode, - templateAttrs, jqCollection, childTranscludeFn, preLinkFns, postLinkFns, { + templateAttrs, jqCollection, hasTranscludeDirective && childTranscludeFn, preLinkFns, postLinkFns, { controllerDirectives: controllerDirectives, newIsolateScopeDirective: newIsolateScopeDirective, templateDirective: templateDirective, @@ -1324,7 +1340,10 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { } nodeLinkFn.scope = newScopeDirective && newScopeDirective.scope === true; - nodeLinkFn.transclude = hasTranscludeDirective && childTranscludeFn; + nodeLinkFn.transcludeOnThisElement = hasTranscludeDirective; + nodeLinkFn.templateOnThisElement = hasTemplate; + nodeLinkFn.transclude = childTranscludeFn; + previousCompileContext.hasElementTranscludeDirective = hasElementTranscludeDirective; // might be normal or delayed nodeLinkFn depending on if templateUrl is present @@ -1719,7 +1738,6 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { }); afterTemplateChildLinkFn = compileNodes($compileNode[0].childNodes, childTranscludeFn); - while(linkQueue.length) { var scope = linkQueue.shift(), beforeTemplateLinkNode = linkQueue.shift(), @@ -1741,8 +1759,8 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { // Copy in CSS classes from original node safeAddClass(jqLite(linkNode), oldClasses); } - if (afterTemplateNodeLinkFn.transclude) { - childBoundTranscludeFn = createBoundTranscludeFn(scope, afterTemplateNodeLinkFn.transclude); + if (afterTemplateNodeLinkFn.transcludeOnThisElement) { + childBoundTranscludeFn = createBoundTranscludeFn(scope, afterTemplateNodeLinkFn.transclude, boundTranscludeFn); } else { childBoundTranscludeFn = boundTranscludeFn; } @@ -1756,13 +1774,17 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { }); return function delayedNodeLinkFn(ignoreChildLinkFn, scope, node, rootElement, boundTranscludeFn) { + var childBoundTranscludeFn = boundTranscludeFn; if (linkQueue) { linkQueue.push(scope); linkQueue.push(node); linkQueue.push(rootElement); - linkQueue.push(boundTranscludeFn); + linkQueue.push(childBoundTranscludeFn); } else { - afterTemplateNodeLinkFn(afterTemplateChildLinkFn, scope, node, rootElement, boundTranscludeFn); + if (afterTemplateNodeLinkFn.transcludeOnThisElement) { + childBoundTranscludeFn = createBoundTranscludeFn(scope, afterTemplateNodeLinkFn.transclude, boundTranscludeFn); + } + afterTemplateNodeLinkFn(afterTemplateChildLinkFn, scope, node, rootElement, childBoundTranscludeFn); } }; } diff --git a/test/ng/compileSpec.js b/test/ng/compileSpec.js index 496ff8dada3d..480bde531a1d 100755 --- a/test/ng/compileSpec.js +++ b/test/ng/compileSpec.js @@ -3764,6 +3764,88 @@ describe('$compile', function() { }); + it('should not pass transclusion into a template directive when the directive didn\'t request transclusion', function() { + + module(function($compileProvider) { + + $compileProvider.directive('transFoo', valueFn({ + template: '
' + + '
' + + '
this one should get replaced with content
' + + '
' + + '
', + transclude: true + + })); + + $compileProvider.directive('noTransBar', valueFn({ + template: '
' + + // This ng-transclude is invalid. It should throw an error. + '
' + + '
', + transclude: false + + })); + }); + + inject(function($compile, $rootScope) { + var message = 'Illegal use of ngTransclude directive in the template! No parent ' + + 'directive that requires a transclusion found. Element:
'; + if (msie <= 8) { + // MSIE ヽ(`Д´)ノ + message = 'Illegal use of ngTransclude directive in the template! No parent ' + + 'directive that requires a transclusion found. Element:
'; + } + expect(function() { + $compile('
content
')($rootScope); + }).toThrowMinErr('ngTransclude', 'orphan', message); + }); + }); + + + it('should not pass transclusion into a templateUrl directive', function() { + + module(function($compileProvider) { + + $compileProvider.directive('transFoo', valueFn({ + template: '
' + + '
' + + '
this one should get replaced with content
' + + '
' + + '
', + transclude: true + + })); + + $compileProvider.directive('noTransBar', valueFn({ + templateUrl: 'noTransBar.html', + transclude: false + + })); + }); + + inject(function($compile, $rootScope, $templateCache) { + $templateCache.put('noTransBar.html', + '
' + + // This ng-transclude is invalid. It should throw an error. + '
' + + '
'); + var message = 'Illegal use of ngTransclude directive in the template! No parent directive that ' + + 'requires a transclusion found. Element:
'; + if (msie <= 8) { + // MSIE ヽ(`Д´)ノ + message = 'Illegal use of ngTransclude directive in the template! No parent directive that ' + + 'requires a transclusion found. Element:
'; + } + expect(function() { + element = $compile('
content
')($rootScope); + $rootScope.$apply(); + }).toThrowMinErr('ngTransclude', 'orphan', message); + }); + }); + it('should make the result of a transclusion available to the parent directive in post-linking phase' + '(template)', function() { module(function() { @@ -3982,6 +4064,182 @@ describe('$compile', function() { }); }); + + + describe('nested transcludes', function() { + + beforeEach(module(function($compileProvider) { + + $compileProvider.directive('noop', valueFn({})); + + $compileProvider.directive('sync', valueFn({ + template: '
', + transclude: true + })); + + $compileProvider.directive('async', valueFn({ + templateUrl: 'async', + transclude: true + })); + + $compileProvider.directive('syncSync', valueFn({ + template: '
', + transclude: true + })); + + $compileProvider.directive('syncAsync', valueFn({ + template: '
', + transclude: true + })); + + $compileProvider.directive('asyncSync', valueFn({ + templateUrl: 'asyncSync', + transclude: true + })); + + $compileProvider.directive('asyncAsync', valueFn({ + templateUrl: 'asyncAsync', + transclude: true + })); + + })); + + beforeEach(inject(function($templateCache) { + $templateCache.put('async', '
'); + $templateCache.put('asyncSync', '
'); + $templateCache.put('asyncAsync', '
'); + })); + + + it('should allow nested transclude directives with sync template containing sync template', inject(function($compile, $rootScope) { + element = $compile('
transcluded content
')($rootScope); + $rootScope.$digest(); + expect(element.text()).toEqual('transcluded content'); + })); + + it('should allow nested transclude directives with sync template containing async template', inject(function($compile, $rootScope) { + element = $compile('
transcluded content
')($rootScope); + $rootScope.$digest(); + expect(element.text()).toEqual('transcluded content'); + })); + + it('should allow nested transclude directives with async template containing sync template', inject(function($compile, $rootScope) { + element = $compile('
transcluded content
')($rootScope); + $rootScope.$digest(); + expect(element.text()).toEqual('transcluded content'); + })); + + it('should allow nested transclude directives with async template containing asynch template', inject(function($compile, $rootScope) { + element = $compile('
transcluded content
')($rootScope); + $rootScope.$digest(); + expect(element.text()).toEqual('transcluded content'); + })); + }); + + + describe('nested isolated scope transcludes', function() { + beforeEach(module(function($compileProvider) { + + $compileProvider.directive('trans', valueFn({ + restrict: 'E', + template: '
', + transclude: true + })); + + $compileProvider.directive('transAsync', valueFn({ + restrict: 'A', + templateUrl: 'transAsync', + transclude: true + })); + + $compileProvider.directive('iso', valueFn({ + restrict: 'A', + transclude: true, + template: '
', + scope: {} + })); + $compileProvider.directive('isoAsync1', valueFn({ + restrict: 'A', + transclude: true, + template: '
', + scope: {} + })); + $compileProvider.directive('isoAsync2', valueFn({ + restrict: 'A', + transclude: true, + templateUrl: 'isoAsync', + scope: {} + })); + })); + + beforeEach(inject(function($templateCache) { + $templateCache.put('transAsync', '
'); + $templateCache.put('isoAsync', '
'); + })); + + + it('should pass the outer scope to the transclude on the isolated template sync-sync', inject(function($compile, $rootScope) { + + $rootScope.val = 'transcluded content'; + element = $compile('
')($rootScope); + $rootScope.$digest(); + expect(element.text()).toEqual('transcluded content'); + })); + + it('should pass the outer scope to the transclude on the isolated template async-sync', inject(function($compile, $rootScope) { + + $rootScope.val = 'transcluded content'; + element = $compile('
')($rootScope); + $rootScope.$digest(); + expect(element.text()).toEqual('transcluded content'); + })); + + it('should pass the outer scope to the transclude on the isolated template async-async', inject(function($compile, $rootScope) { + + $rootScope.val = 'transcluded content'; + element = $compile('
')($rootScope); + $rootScope.$digest(); + expect(element.text()).toEqual('transcluded content'); + })); + + }); + + describe('multiple siblings receiving transclusion', function() { + + it("should only receive transclude from parent", function() { + + module(function($compileProvider) { + + $compileProvider.directive('myExample', valueFn({ + scope: {}, + link: function link(scope, element, attrs) { + var foo = element[0].querySelector('.foo'); + scope.children = angular.element(foo).children().length; + }, + template: '
' + + '
myExample {{children}}!
' + + '
has children
' + + '
' + + '
', + transclude: true + + })); + + }); + + inject(function($compile, $rootScope) { + var element = $compile('
')($rootScope); + $rootScope.$digest(); + expect(element.text()).toEqual('myExample 0!'); + dealoc(element); + + element = $compile('

')($rootScope); + $rootScope.$digest(); + expect(element.text()).toEqual('myExample 1!has children'); + dealoc(element); + }); + }); + }); }); @@ -4318,6 +4576,57 @@ describe('$compile', function() { expect(element.text()).toBe('-->|x|'); })); + + + // See https://github.com/angular/angular.js/issues/7183 + it("should pass transclusion through to template of a 'replace' directive", function() { + module(function() { + directive('transSync', function() { + return { + transclude: true, + link: function(scope, element, attr, ctrl, transclude) { + + expect(transclude).toEqual(jasmine.any(Function)); + + transclude(function(child) { element.append(child); }); + } + }; + }); + + directive('trans', function($timeout) { + return { + transclude: true, + link: function(scope, element, attrs, ctrl, transclude) { + + // We use timeout here to simulate how ng-if works + $timeout(function() { + transclude(function(child) { element.append(child); }); + }); + } + }; + }); + + directive('replaceWithTemplate', function() { + return { + templateUrl: "template.html", + replace: true + }; + }); + }); + + inject(function($compile, $rootScope, $templateCache, $timeout) { + + $templateCache.put('template.html', '
Content To Be Transcluded
'); + + expect(function() { + element = $compile('
')($rootScope); + $timeout.flush(); + }).not.toThrow(); + + expect(element.text()).toEqual('Content To Be Transcluded'); + }); + + }); }); diff --git a/test/ng/directive/ngIfSpec.js b/test/ng/directive/ngIfSpec.js index 771e264a9a73..3a8beaef4b3b 100755 --- a/test/ng/directive/ngIfSpec.js +++ b/test/ng/directive/ngIfSpec.js @@ -199,6 +199,28 @@ describe('ngIf and transcludes', function() { dealoc(element); }); }); + + + it('should use the correct transcluded scope', function() { + module(function($compileProvider) { + $compileProvider.directive('iso', valueFn({ + link: function(scope) { + scope.val = 'value in iso scope'; + }, + restrict: 'A', + transclude: true, + template: '
val={{val}}-
', + scope: {} + })); + }); + inject(function($compile, $rootScope) { + $rootScope.val = 'transcluded content'; + var element = $compile('
')($rootScope); + $rootScope.$digest(); + expect(trim(element.text())).toEqual('val=value in iso scope-transcluded content'); + dealoc(element); + }); + }); }); describe('ngIf animations', function () {