From 742271ffa3a518d9e8ef2cb97c24b45b44e3378d Mon Sep 17 00:00:00 2001 From: Vojta Jina Date: Sun, 8 Sep 2013 10:05:25 -0700 Subject: [PATCH] fix($compile): link parents before traversing How did compiling a templateUrl (async) directive with `replace:true` work before this commit? 1/ apply all directives with higher priority than the templateUrl directive 2/ partially apply the templateUrl directive (create `beforeTemplateNodeLinkFn`) 3/ fetch the template 4/ apply second part of the templateUrl directive on the fetched template (`afterTemplateNodeLinkFn`) That is, the templateUrl directive is basically split into two parts (two `nodeLinkFn` functions), which has to be both applied. Normally we compose linking functions (`nodeLinkFn`) using continuation - calling the linking function of a parent element, passing the linking function of the child elements as an argument. The parent linking function then does: 1/ execute its pre-link functions 2/ call the child elements linking function (traverse) 3/ execute its post-link functions Now, we have two linking functions for the same DOM element level (because the templateUrl directive has been split). There has been multiple issues because of the order of these two linking functions (creating controller before setting up scope locals, running linking functions before instantiating controller, etc.). It is easy to fix one use case, but it breaks some other use case. It is hard to decide what is the "correct" order of these two linking functions as they are essentially on the same level. Running them side-by-side screws up pre/post linking functions for the high priority directives (those executed before the templateUrl directive). It runs post-linking functions before traversing: ```js beforeTemplateNodeLinkFn(null); // do not travers afterTemplateNodeLinkFn(afterTemplateChildLinkFn); ``` Composing them (in any order) screws up the order of post-linking functions. We could fix this by having post-linking functions to execute in reverse order (from the lowest priority to the highest) which might actually make a sense. **My solution is to remove this splitting.** This commit removes the `beforeTemplateNodeLinkFn`. The first run (before we have the template) only schedules fetching the template. The rest (creating scope locals, instantiating a controller, linking functions, etc) is done when processing the directive again (in the context of the already fetched template; this is the cloned `derivedSyncDirective`). We still need to pass-through the linking functions of the higher priority directives (those executed before the templateUrl directive), that's why I added `preLinkFns` and `postLinkFns` arguments to `applyDirectivesToNode`. This also changes the "$compile transclude should make the result of a transclusion available to the parent directive in post- linking phase (templateUrl)" unit test. It was testing that a parent directive can see the content of transclusion in its pre-link function. That is IMHO wrong (as the `ngTransclude` directive inserts the translusion in its linking function). This test was only passing because of c173ca412878d537b18df01f39e400ea48a4b398, which changed the behavior of the compiler to traverse before executing the parent linking function. That was wrong and also caused the #3792 issue, which this change fixes. Closes #3792 Closes #3923 Closes #3935 Closes #3927 --- src/ng/compile.js | 54 +++++++++++++++++++++++------------------- test/ng/compileSpec.js | 52 +++++++++++++++++++++++++++++++++------- 2 files changed, 74 insertions(+), 32 deletions(-) diff --git a/src/ng/compile.js b/src/ng/compile.js index c49fbe283ec0..040836f72dad 100644 --- a/src/ng/compile.js +++ b/src/ng/compile.js @@ -536,7 +536,7 @@ function $CompileProvider($provide) { directives = collectDirectives(nodeList[i], [], attrs, i == 0 ? maxPriority : undefined, ignoreDirective); nodeLinkFn = (directives.length) - ? applyDirectivesToNode(directives, nodeList[i], attrs, transcludeFn, $rootElement) + ? applyDirectivesToNode(directives, nodeList[i], attrs, transcludeFn, $rootElement, null, [], []) : null; childLinkFn = (nodeLinkFn && nodeLinkFn.terminal || !nodeList[i].childNodes || !nodeList[i].childNodes.length) @@ -747,12 +747,15 @@ function $CompileProvider($provide) { * scope argument is auto-generated to the new child of the transcluded parent scope. * @param {JQLite} jqCollection If we are working on the root of the compile tree then this * argument has the root jqLite array so that we can replace nodes on it. + * @param {Object=} ignoreDirective An optional directive that will be ignored when compiling + * the transclusion. + * @param {Array.} preLinkFns + * @param {Array.} postLinkFns * @returns linkFn */ - function applyDirectivesToNode(directives, compileNode, templateAttrs, transcludeFn, jqCollection, originalReplaceDirective) { + function applyDirectivesToNode(directives, compileNode, templateAttrs, transcludeFn, jqCollection, + originalReplaceDirective, preLinkFns, postLinkFns) { var terminalPriority = -Number.MAX_VALUE, - preLinkFns = [], - postLinkFns = [], newScopeDirective = null, newIsolateScopeDirective = null, templateDirective = null, @@ -784,18 +787,24 @@ function $CompileProvider($provide) { } if (directiveValue = directive.scope) { - assertNoDuplicate('isolated scope', newIsolateScopeDirective, directive, $compileNode); - if (isObject(directiveValue)) { - safeAddClass($compileNode, 'ng-isolate-scope'); - newIsolateScopeDirective = directive; - } - safeAddClass($compileNode, 'ng-scope'); newScopeDirective = newScopeDirective || directive; + + // skip the check for directives with async templates, we'll check the derived sync directive when + // the template arrives + if (!directive.templateUrl) { + assertNoDuplicate('new/isolated scope', newIsolateScopeDirective, directive, $compileNode); + if (isObject(directiveValue)) { + safeAddClass($compileNode, 'ng-isolate-scope'); + newIsolateScopeDirective = directive; + } + safeAddClass($compileNode, 'ng-scope'); + } } directiveName = directive.name; - if (directiveValue = directive.controller) { + if (!directive.templateUrl && directive.controller) { + directiveValue = directive.controller; controllerDirectives = controllerDirectives || {}; assertNoDuplicate("'" + directiveName + "' controller", controllerDirectives[directiveName], directive, $compileNode); @@ -874,8 +883,9 @@ function $CompileProvider($provide) { if (directive.replace) { replaceDirective = directive; } - nodeLinkFn = compileTemplateUrl(directives.splice(i, directives.length - i), - nodeLinkFn, $compileNode, templateAttrs, jqCollection, childTranscludeFn); + + nodeLinkFn = compileTemplateUrl(directives.splice(i, directives.length - i), $compileNode, + templateAttrs, jqCollection, childTranscludeFn, preLinkFns, postLinkFns); ii = directives.length; } else if (directive.compile) { try { @@ -1170,8 +1180,8 @@ function $CompileProvider($provide) { } - function compileTemplateUrl(directives, beforeTemplateNodeLinkFn, $compileNode, tAttrs, - $rootElement, childTranscludeFn) { + function compileTemplateUrl(directives, $compileNode, tAttrs, + $rootElement, childTranscludeFn, preLinkFns, postLinkFns) { var linkQueue = [], afterTemplateNodeLinkFn, afterTemplateChildLinkFn, @@ -1179,7 +1189,7 @@ function $CompileProvider($provide) { origAsyncDirective = directives.shift(), // The fact that we have to copy and patch the directive seems wrong! derivedSyncDirective = extend({}, origAsyncDirective, { - controller: null, templateUrl: null, transclude: null, scope: null, replace: null + templateUrl: null, transclude: null, replace: null }), templateUrl = (isFunction(origAsyncDirective.templateUrl)) ? origAsyncDirective.templateUrl($compileNode, tAttrs) @@ -1213,7 +1223,8 @@ function $CompileProvider($provide) { directives.unshift(derivedSyncDirective); - afterTemplateNodeLinkFn = applyDirectivesToNode(directives, compileNode, tAttrs, childTranscludeFn, $compileNode, origAsyncDirective); + afterTemplateNodeLinkFn = applyDirectivesToNode(directives, compileNode, tAttrs, + childTranscludeFn, $compileNode, origAsyncDirective, preLinkFns, postLinkFns); forEach($rootElement, function(node, i) { if (node == compileNode) { $rootElement[i] = $compileNode[0]; @@ -1235,10 +1246,7 @@ function $CompileProvider($provide) { replaceWith(linkRootElement, jqLite(beforeTemplateLinkNode), linkNode); } - afterTemplateNodeLinkFn( - beforeTemplateNodeLinkFn(afterTemplateChildLinkFn, scope, linkNode, $rootElement, controller), - scope, linkNode, $rootElement, controller - ); + afterTemplateNodeLinkFn(afterTemplateChildLinkFn, scope, linkNode, $rootElement, controller); } linkQueue = null; }). @@ -1253,9 +1261,7 @@ function $CompileProvider($provide) { linkQueue.push(rootElement); linkQueue.push(controller); } else { - afterTemplateNodeLinkFn(function() { - beforeTemplateNodeLinkFn(afterTemplateChildLinkFn, scope, node, rootElement, controller); - }, scope, node, rootElement, controller); + afterTemplateNodeLinkFn(afterTemplateChildLinkFn, scope, node, rootElement, controller); } }; } diff --git a/test/ng/compileSpec.js b/test/ng/compileSpec.js index 2d6485d9bf35..c6ba5480cd64 100755 --- a/test/ng/compileSpec.js +++ b/test/ng/compileSpec.js @@ -1046,7 +1046,10 @@ describe('$compile', function() { priority: priority, compile: function() { log(name + '-C'); - return function() { log(name + '-L'); } + return { + pre: function() { log(name + '-PreL'); }, + post: function() { log(name + '-PostL'); } + } } }, options || {})); }); @@ -1075,7 +1078,8 @@ describe('$compile', function() { $rootScope.$digest(); expect(log).toEqual( 'first-C; FLUSH; second-C; last-C; third-C; ' + - 'third-L; first-L; second-L; last-L'); + 'first-PreL; second-PreL; last-PreL; third-PreL; ' + + 'third-PostL; first-PostL; second-PostL; last-PostL'); var span = element.find('span'); expect(span.attr('first')).toEqual(''); @@ -1099,7 +1103,8 @@ describe('$compile', function() { $rootScope.$digest(); expect(log).toEqual( 'iFirst-C; FLUSH; iSecond-C; iThird-C; iLast-C; ' + - 'iFirst-L; iSecond-L; iThird-L; iLast-L'); + 'iFirst-PreL; iSecond-PreL; iThird-PreL; iLast-PreL; ' + + 'iFirst-PostL; iSecond-PostL; iThird-PostL; iLast-PostL'); var div = element.find('div'); expect(div.attr('i-first')).toEqual(''); @@ -1124,7 +1129,8 @@ describe('$compile', function() { $rootScope.$digest(); expect(log).toEqual( 'first-C; FLUSH; second-C; last-C; third-C; ' + - 'third-L; first-L; second-L; last-L'); + 'first-PreL; second-PreL; last-PreL; third-PreL; ' + + 'third-PostL; first-PostL; second-PostL; last-PostL'); var span = element.find('span'); expect(span.attr('first')).toEqual(''); @@ -1149,7 +1155,8 @@ describe('$compile', function() { $rootScope.$digest(); expect(log).toEqual( 'iFirst-C; FLUSH; iSecond-C; iThird-C; iLast-C; ' + - 'iFirst-L; iSecond-L; iThird-L; iLast-L'); + 'iFirst-PreL; iSecond-PreL; iThird-PreL; iLast-PreL; ' + + 'iFirst-PostL; iSecond-PostL; iThird-PostL; iLast-PostL'); var div = element.find('div'); expect(div.attr('i-first')).toEqual(''); @@ -1264,6 +1271,35 @@ describe('$compile', function() { expect(element.text()).toBe('boom!1|boom!2|'); }); }); + + + it('should support templateUrl with replace', function() { + // a regression https://github.com/angular/angular.js/issues/3792 + module(function($compileProvider) { + $compileProvider.directive('simple', function() { + return { + templateUrl: '/some.html', + replace: true + }; + }); + }); + + inject(function($templateCache, $rootScope, $compile) { + $templateCache.put('/some.html', + '
' + + '
i = 1
' + + '
I dont know what `i` is.
' + + '
'); + + element = $compile('
')($rootScope); + + $rootScope.$apply(function() { + $rootScope.i = 1; + }); + + expect(element.html()).toContain('i = 1'); + }); + }); }); @@ -1482,7 +1518,7 @@ describe('$compile', function() { function($rootScope, $compile) { expect(function(){ $compile('
'); - }).toThrowMinErr('$compile', 'multidir', 'Multiple directives [iscopeA, scopeB] asking for isolated scope on: ' + + }).toThrowMinErr('$compile', 'multidir', 'Multiple directives [iscopeA, scopeB] asking for new/isolated scope on: ' + '
'); }) ); @@ -2824,7 +2860,7 @@ describe('$compile', function() { }); - it('should make the result of a transclusion available to the parent directive in pre- and post- linking phase (templateUrl)', + it('should make the result of a transclusion available to the parent directive in post-linking phase (templateUrl)', function() { // when compiling an async directive the transclusion is always processed before the directive // this is different compared to sync directive. delaying the transclusion makes little sense. @@ -2850,7 +2886,7 @@ describe('$compile', function() { element = $compile('
unicorn!
')($rootScope); $rootScope.$apply(); - expect(log).toEqual('pre(unicorn!); post(unicorn!)'); + expect(log).toEqual('pre(); post(unicorn!)'); }); }); });