Skip to content

Commit 894b9ab

Browse files
vojtajinaIgorMinar
authored andcommitted
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 c173ca4, which changed the behavior of the compiler to traverse before executing the parent linking function. That was wrong and also caused the angular#3792 issue, which this change fixes. Closes angular#3792
1 parent f2160a4 commit 894b9ab

File tree

2 files changed

+74
-32
lines changed

2 files changed

+74
-32
lines changed

src/ng/compile.js

Lines changed: 30 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -535,7 +535,7 @@ function $CompileProvider($provide) {
535535
directives = collectDirectives(nodeList[i], [], attrs, i == 0 ? maxPriority : undefined, ignoreDirective);
536536

537537
nodeLinkFn = (directives.length)
538-
? applyDirectivesToNode(directives, nodeList[i], attrs, transcludeFn, $rootElement)
538+
? applyDirectivesToNode(directives, nodeList[i], attrs, transcludeFn, $rootElement, null, [], [])
539539
: null;
540540

541541
childLinkFn = (nodeLinkFn && nodeLinkFn.terminal || !nodeList[i].childNodes || !nodeList[i].childNodes.length)
@@ -746,12 +746,15 @@ function $CompileProvider($provide) {
746746
* scope argument is auto-generated to the new child of the transcluded parent scope.
747747
* @param {JQLite} jqCollection If we are working on the root of the compile tree then this
748748
* argument has the root jqLite array so that we can replace nodes on it.
749+
* @param {Object=} ignoreDirective An optional directive that will be ignored when compiling
750+
* the transclusion.
751+
* @param {Array.<Function>} preLinkFns
752+
* @param {Array.<Function>} postLinkFns
749753
* @returns linkFn
750754
*/
751-
function applyDirectivesToNode(directives, compileNode, templateAttrs, transcludeFn, jqCollection, originalReplaceDirective) {
755+
function applyDirectivesToNode(directives, compileNode, templateAttrs, transcludeFn, jqCollection,
756+
originalReplaceDirective, preLinkFns, postLinkFns) {
752757
var terminalPriority = -Number.MAX_VALUE,
753-
preLinkFns = [],
754-
postLinkFns = [],
755758
newScopeDirective = null,
756759
newIsolateScopeDirective = null,
757760
templateDirective = null,
@@ -783,18 +786,24 @@ function $CompileProvider($provide) {
783786
}
784787

785788
if (directiveValue = directive.scope) {
786-
assertNoDuplicate('isolated scope', newIsolateScopeDirective, directive, $compileNode);
787-
if (isObject(directiveValue)) {
788-
safeAddClass($compileNode, 'ng-isolate-scope');
789-
newIsolateScopeDirective = directive;
790-
}
791-
safeAddClass($compileNode, 'ng-scope');
792789
newScopeDirective = newScopeDirective || directive;
790+
791+
// skip the check for directives with async templates, we'll check the derived sync directive when
792+
// the template arrives
793+
if (!directive.templateUrl) {
794+
assertNoDuplicate('new/isolated scope', newIsolateScopeDirective, directive, $compileNode);
795+
if (isObject(directiveValue)) {
796+
safeAddClass($compileNode, 'ng-isolate-scope');
797+
newIsolateScopeDirective = directive;
798+
}
799+
safeAddClass($compileNode, 'ng-scope');
800+
}
793801
}
794802

795803
directiveName = directive.name;
796804

797-
if (directiveValue = directive.controller) {
805+
if (!directive.templateUrl && directive.controller) {
806+
directiveValue = directive.controller;
798807
controllerDirectives = controllerDirectives || {};
799808
assertNoDuplicate("'" + directiveName + "' controller",
800809
controllerDirectives[directiveName], directive, $compileNode);
@@ -873,8 +882,9 @@ function $CompileProvider($provide) {
873882
if (directive.replace) {
874883
replaceDirective = directive;
875884
}
876-
nodeLinkFn = compileTemplateUrl(directives.splice(i, directives.length - i),
877-
nodeLinkFn, $compileNode, templateAttrs, jqCollection, childTranscludeFn);
885+
886+
nodeLinkFn = compileTemplateUrl(directives.splice(i, directives.length - i), $compileNode,
887+
templateAttrs, jqCollection, childTranscludeFn, preLinkFns, postLinkFns);
878888
ii = directives.length;
879889
} else if (directive.compile) {
880890
try {
@@ -1169,16 +1179,16 @@ function $CompileProvider($provide) {
11691179
}
11701180

11711181

1172-
function compileTemplateUrl(directives, beforeTemplateNodeLinkFn, $compileNode, tAttrs,
1173-
$rootElement, childTranscludeFn) {
1182+
function compileTemplateUrl(directives, $compileNode, tAttrs,
1183+
$rootElement, childTranscludeFn, preLinkFns, postLinkFns) {
11741184
var linkQueue = [],
11751185
afterTemplateNodeLinkFn,
11761186
afterTemplateChildLinkFn,
11771187
beforeTemplateCompileNode = $compileNode[0],
11781188
origAsyncDirective = directives.shift(),
11791189
// The fact that we have to copy and patch the directive seems wrong!
11801190
derivedSyncDirective = extend({}, origAsyncDirective, {
1181-
controller: null, templateUrl: null, transclude: null, scope: null, replace: null
1191+
templateUrl: null, transclude: null, replace: null
11821192
}),
11831193
templateUrl = (isFunction(origAsyncDirective.templateUrl))
11841194
? origAsyncDirective.templateUrl($compileNode, tAttrs)
@@ -1212,7 +1222,8 @@ function $CompileProvider($provide) {
12121222

12131223
directives.unshift(derivedSyncDirective);
12141224

1215-
afterTemplateNodeLinkFn = applyDirectivesToNode(directives, compileNode, tAttrs, childTranscludeFn, $compileNode, origAsyncDirective);
1225+
afterTemplateNodeLinkFn = applyDirectivesToNode(directives, compileNode, tAttrs,
1226+
childTranscludeFn, $compileNode, origAsyncDirective, preLinkFns, postLinkFns);
12161227
forEach($rootElement, function(node, i) {
12171228
if (node == compileNode) {
12181229
$rootElement[i] = $compileNode[0];
@@ -1234,10 +1245,7 @@ function $CompileProvider($provide) {
12341245
replaceWith(linkRootElement, jqLite(beforeTemplateLinkNode), linkNode);
12351246
}
12361247

1237-
afterTemplateNodeLinkFn(
1238-
beforeTemplateNodeLinkFn(afterTemplateChildLinkFn, scope, linkNode, $rootElement, controller),
1239-
scope, linkNode, $rootElement, controller
1240-
);
1248+
afterTemplateNodeLinkFn(afterTemplateChildLinkFn, scope, linkNode, $rootElement, controller);
12411249
}
12421250
linkQueue = null;
12431251
}).
@@ -1252,9 +1260,7 @@ function $CompileProvider($provide) {
12521260
linkQueue.push(rootElement);
12531261
linkQueue.push(controller);
12541262
} else {
1255-
afterTemplateNodeLinkFn(function() {
1256-
beforeTemplateNodeLinkFn(afterTemplateChildLinkFn, scope, node, rootElement, controller);
1257-
}, scope, node, rootElement, controller);
1263+
afterTemplateNodeLinkFn(afterTemplateChildLinkFn, scope, node, rootElement, controller);
12581264
}
12591265
};
12601266
}

test/ng/compileSpec.js

Lines changed: 44 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1046,7 +1046,10 @@ describe('$compile', function() {
10461046
priority: priority,
10471047
compile: function() {
10481048
log(name + '-C');
1049-
return function() { log(name + '-L'); }
1049+
return {
1050+
pre: function() { log(name + '-PreL'); },
1051+
post: function() { log(name + '-PostL'); }
1052+
}
10501053
}
10511054
}, options || {}));
10521055
});
@@ -1075,7 +1078,8 @@ describe('$compile', function() {
10751078
$rootScope.$digest();
10761079
expect(log).toEqual(
10771080
'first-C; FLUSH; second-C; last-C; third-C; ' +
1078-
'third-L; first-L; second-L; last-L');
1081+
'first-PreL; second-PreL; last-PreL; third-PreL; ' +
1082+
'third-PostL; first-PostL; second-PostL; last-PostL');
10791083

10801084
var span = element.find('span');
10811085
expect(span.attr('first')).toEqual('');
@@ -1099,7 +1103,8 @@ describe('$compile', function() {
10991103
$rootScope.$digest();
11001104
expect(log).toEqual(
11011105
'iFirst-C; FLUSH; iSecond-C; iThird-C; iLast-C; ' +
1102-
'iFirst-L; iSecond-L; iThird-L; iLast-L');
1106+
'iFirst-PreL; iSecond-PreL; iThird-PreL; iLast-PreL; ' +
1107+
'iFirst-PostL; iSecond-PostL; iThird-PostL; iLast-PostL');
11031108

11041109
var div = element.find('div');
11051110
expect(div.attr('i-first')).toEqual('');
@@ -1124,7 +1129,8 @@ describe('$compile', function() {
11241129
$rootScope.$digest();
11251130
expect(log).toEqual(
11261131
'first-C; FLUSH; second-C; last-C; third-C; ' +
1127-
'third-L; first-L; second-L; last-L');
1132+
'first-PreL; second-PreL; last-PreL; third-PreL; ' +
1133+
'third-PostL; first-PostL; second-PostL; last-PostL');
11281134

11291135
var span = element.find('span');
11301136
expect(span.attr('first')).toEqual('');
@@ -1149,7 +1155,8 @@ describe('$compile', function() {
11491155
$rootScope.$digest();
11501156
expect(log).toEqual(
11511157
'iFirst-C; FLUSH; iSecond-C; iThird-C; iLast-C; ' +
1152-
'iFirst-L; iSecond-L; iThird-L; iLast-L');
1158+
'iFirst-PreL; iSecond-PreL; iThird-PreL; iLast-PreL; ' +
1159+
'iFirst-PostL; iSecond-PostL; iThird-PostL; iLast-PostL');
11531160

11541161
var div = element.find('div');
11551162
expect(div.attr('i-first')).toEqual('');
@@ -1264,6 +1271,35 @@ describe('$compile', function() {
12641271
expect(element.text()).toBe('boom!1|boom!2|');
12651272
});
12661273
});
1274+
1275+
1276+
it('should support templateUrl with replace', function() {
1277+
// a regression https://github.com/angular/angular.js/issues/3792
1278+
module(function($compileProvider) {
1279+
$compileProvider.directive('simple', function() {
1280+
return {
1281+
templateUrl: '/some.html',
1282+
replace: true
1283+
};
1284+
});
1285+
});
1286+
1287+
inject(function($templateCache, $rootScope, $compile) {
1288+
$templateCache.put('/some.html',
1289+
'<div ng-switch="i">' +
1290+
'<div ng-switch-when="1">i = 1</div>' +
1291+
'<div ng-switch-default>I dont know what `i` is.</div>' +
1292+
'</div>');
1293+
1294+
element = $compile('<div simple></div>')($rootScope);
1295+
1296+
$rootScope.$apply(function() {
1297+
$rootScope.i = 1;
1298+
});
1299+
1300+
expect(element.html()).toContain('i = 1');
1301+
});
1302+
});
12671303
});
12681304

12691305

@@ -1482,7 +1518,7 @@ describe('$compile', function() {
14821518
function($rootScope, $compile) {
14831519
expect(function(){
14841520
$compile('<div class="iscope-a; scope-b"></div>');
1485-
}).toThrowMinErr('$compile', 'multidir', 'Multiple directives [iscopeA, scopeB] asking for isolated scope on: ' +
1521+
}).toThrowMinErr('$compile', 'multidir', 'Multiple directives [iscopeA, scopeB] asking for new/isolated scope on: ' +
14861522
'<div class="iscope-a; scope-b ng-isolate-scope ng-scope">');
14871523
})
14881524
);
@@ -2824,7 +2860,7 @@ describe('$compile', function() {
28242860
});
28252861

28262862

2827-
it('should make the result of a transclusion available to the parent directive in pre- and post- linking phase (templateUrl)',
2863+
it('should make the result of a transclusion available to the parent directive in post-linking phase (templateUrl)',
28282864
function() {
28292865
// when compiling an async directive the transclusion is always processed before the directive
28302866
// this is different compared to sync directive. delaying the transclusion makes little sense.
@@ -2850,7 +2886,7 @@ describe('$compile', function() {
28502886

28512887
element = $compile('<div trans><span>unicorn!</span></div>')($rootScope);
28522888
$rootScope.$apply();
2853-
expect(log).toEqual('pre(unicorn!); post(unicorn!)');
2889+
expect(log).toEqual('pre(); post(unicorn!)');
28542890
});
28552891
});
28562892
});

0 commit comments

Comments
 (0)