Skip to content
This repository was archived by the owner on Apr 12, 2024. It is now read-only.

Commit 841c090

Browse files
dlongleypetebacondarwin
authored andcommitted
fix($compile): do not rebind parent bound transclude functions
The `$compile` public API documentation indicates that a transclude function may be passed as a second parameter, but it was not clear that this is **not** the same function that is given to directive link functions as the `transcludeFn` parameter. We would like to be able to pass in a transclude function the public linking function that is returned from `$compile` if we wish to, for example, use lazy compilation inside a directive. Doing so, however, highlighted two bugs: * First, the transclude function would get rebound, incorrectly, changing its scope from its original binding. * Second, the `containingScope` would not be updated and the wrong `$parent` would be assigned to the `transcludedScope` resulting in a memory leak. This patch fixes both of these issues. It also converts various private optional positional parameters on `publicLinkFn` into an `options` parameter, which is an object hash. The new `options` parameter is documented as part of the public API. Thanks to @lgalfaso, @tbosch, and @petebacondarwin. Closes #9413
1 parent da96054 commit 841c090

File tree

3 files changed

+161
-23
lines changed

3 files changed

+161
-23
lines changed

src/ng/compile.js

+49-5
Original file line numberDiff line numberDiff line change
@@ -622,10 +622,17 @@
622622
*
623623
*
624624
* @param {string|DOMElement} element Element or HTML string to compile into a template function.
625-
* @param {function(angular.Scope, cloneAttachFn=)} transclude function available to directives.
625+
* @param {function(angular.Scope, cloneAttachFn=)} transclude function available to directives - DEPRECATED.
626+
*
627+
* <div class="alert alert-error">
628+
* **Note:** Passing a `transclude` function to the $compile function is deprecated, as it
629+
* e.g. will not use the right outer scope. Please pass the transclude function as a
630+
* `parentBoundTranscludeFn` to the link function instead.
631+
* </div>
632+
*
626633
* @param {number} maxPriority only apply directives lower than given priority (Only effects the
627634
* root element(s), not their children)
628-
* @returns {function(scope, cloneAttachFn=)} a link function which is used to bind template
635+
* @returns {function(scope, cloneAttachFn=, options=)} a link function which is used to bind template
629636
* (a DOM element/tree) to a scope. Where:
630637
*
631638
* * `scope` - A {@link ng.$rootScope.Scope Scope} to bind to.
@@ -637,6 +644,19 @@
637644
* * `clonedElement` - is a clone of the original `element` passed into the compiler.
638645
* * `scope` - is the current scope with which the linking function is working with.
639646
*
647+
* * `options` - An optional object hash with linking options. If `options` is provided, then the following
648+
* keys may be used to control linking behavior:
649+
*
650+
* * `parentBoundTranscludeFn` - the transclude function made available to
651+
* directives; if given, it will be passed through to the link functions of
652+
* directives found in `element` during compilation.
653+
* * `transcludeControllers` - an object hash with keys that map controller names
654+
* to controller instances; if given, it will make the controllers
655+
* available to directives.
656+
* * `futureParentElement` - defines the parent to which the `cloneAttachFn` will add
657+
* the cloned elements; only needed for transcludes that are allowed to contain non html
658+
* elements (e.g. SVG elements). See also the directive.controller property.
659+
*
640660
* Calling the linking function returns the element of the template. It is either the original
641661
* element passed in, or the clone of the element if the `cloneAttachFn` is provided.
642662
*
@@ -1155,8 +1175,22 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
11551175
maxPriority, ignoreDirective, previousCompileContext);
11561176
compile.$$addScopeClass($compileNodes);
11571177
var namespace = null;
1158-
return function publicLinkFn(scope, cloneConnectFn, transcludeControllers, parentBoundTranscludeFn, futureParentElement) {
1178+
return function publicLinkFn(scope, cloneConnectFn, options) {
11591179
assertArg(scope, 'scope');
1180+
1181+
options = options || {};
1182+
var parentBoundTranscludeFn = options.parentBoundTranscludeFn,
1183+
transcludeControllers = options.transcludeControllers,
1184+
futureParentElement = options.futureParentElement;
1185+
1186+
// When `parentBoundTranscludeFn` is passed, it is a
1187+
// `controllersBoundTransclude` function (it was previously passed
1188+
// as `transclude` to directive.link) so we must unwrap it to get
1189+
// its `boundTranscludeFn`
1190+
if (parentBoundTranscludeFn && parentBoundTranscludeFn.$$boundTransclude) {
1191+
parentBoundTranscludeFn = parentBoundTranscludeFn.$$boundTransclude;
1192+
}
1193+
11601194
if (!namespace) {
11611195
namespace = detectNamespaceForChildElements(futureParentElement);
11621196
}
@@ -1326,7 +1360,11 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
13261360
transcludedScope.$$transcluded = true;
13271361
}
13281362

1329-
return transcludeFn(transcludedScope, cloneFn, controllers, previousBoundTranscludeFn, futureParentElement);
1363+
return transcludeFn(transcludedScope, cloneFn, {
1364+
parentBoundTranscludeFn: previousBoundTranscludeFn,
1365+
transcludeControllers: controllers,
1366+
futureParentElement: futureParentElement
1367+
});
13301368
};
13311369

13321370
return boundTranscludeFn;
@@ -1794,7 +1832,13 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
17941832
isolateScope = scope.$new(true);
17951833
}
17961834

1797-
transcludeFn = boundTranscludeFn && controllersBoundTransclude;
1835+
if (boundTranscludeFn) {
1836+
// track `boundTranscludeFn` so it can be unwrapped if `transcludeFn`
1837+
// is later passed as `parentBoundTranscludeFn` to `publicLinkFn`
1838+
transcludeFn = controllersBoundTransclude;
1839+
transcludeFn.$$boundTransclude = boundTranscludeFn;
1840+
}
1841+
17981842
if (controllerDirectives) {
17991843
// TODO: merge `controllers` and `elementControllers` into single object.
18001844
controllers = {};

src/ng/directive/ngInclude.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -284,7 +284,7 @@ var ngIncludeFillContentDirective = ['$compile',
284284
$compile(jqLiteBuildFragment(ctrl.template, document).childNodes)(scope,
285285
function namespaceAdaptedClone(clone) {
286286
$element.append(clone);
287-
}, undefined, undefined, $element);
287+
}, {futureParentElement: $element});
288288
return;
289289
}
290290

test/ng/compileSpec.js

+111-17
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,23 @@ describe('$compile', function() {
2626
return !!d.toString().match(/SVGForeignObject/);
2727
}
2828

29+
function countScopes($rootScope) {
30+
return [$rootScope].concat(
31+
getChildScopes($rootScope)
32+
).length;
33+
}
34+
35+
function getChildScopes(scope) {
36+
var children = [];
37+
if (!scope.$$childHead) { return children; }
38+
var childScope = scope.$$childHead;
39+
do {
40+
children.push(childScope);
41+
children = children.concat(getChildScopes(childScope));
42+
} while ((childScope = childScope.$$nextSibling));
43+
return children;
44+
}
45+
2946
var element, directive, $compile, $rootScope;
3047

3148
beforeEach(module(provideLog, function($provide, $compileProvider) {
@@ -5209,25 +5226,102 @@ describe('$compile', function() {
52095226
});
52105227

52115228

5212-
// see issue https://github.com/angular/angular.js/issues/9095
5213-
describe('removing a transcluded element', function() {
5229+
// see issue https://github.com/angular/angular.js/issues/9413
5230+
describe('passing a parent bound transclude function to the link ' +
5231+
'function returned from `$compile`', function() {
52145232

5215-
function countScopes($rootScope) {
5216-
return [$rootScope].concat(
5217-
getChildScopes($rootScope)
5218-
).length;
5219-
}
5233+
beforeEach(module(function() {
5234+
directive('lazyCompile', function($compile) {
5235+
return {
5236+
compile: function(tElement, tAttrs) {
5237+
var content = tElement.contents();
5238+
tElement.empty();
5239+
return function(scope, element, attrs, ctrls, transcludeFn) {
5240+
element.append(content);
5241+
$compile(content)(scope, undefined, {
5242+
parentBoundTranscludeFn: transcludeFn
5243+
});
5244+
};
5245+
}
5246+
};
5247+
});
5248+
directive('toggle', valueFn({
5249+
scope: {t: '=toggle'},
5250+
transclude: true,
5251+
template: '<div ng-if="t"><lazy-compile><div ng-transclude></div></lazy-compile></div>'
5252+
}));
5253+
}));
52205254

5221-
function getChildScopes(scope) {
5222-
var children = [];
5223-
if (!scope.$$childHead) { return children; }
5224-
var childScope = scope.$$childHead;
5225-
do {
5226-
children.push(childScope);
5227-
children = children.concat(getChildScopes(childScope));
5228-
} while ((childScope = childScope.$$nextSibling));
5229-
return children;
5230-
}
5255+
it('should preserve the bound scope', function() {
5256+
5257+
inject(function($compile, $rootScope) {
5258+
element = $compile(
5259+
'<div>' +
5260+
'<div ng-init="outer=true"></div>' +
5261+
'<div toggle="t">' +
5262+
'<span ng-if="outer">Success</span><span ng-if="!outer">Error</span>' +
5263+
'</div>' +
5264+
'</div>')($rootScope);
5265+
5266+
$rootScope.$apply('t = false');
5267+
expect(countScopes($rootScope)).toBe(2);
5268+
expect(element.text()).toBe('');
5269+
5270+
$rootScope.$apply('t = true');
5271+
expect(countScopes($rootScope)).toBe(5);
5272+
expect(element.text()).toBe('Success');
5273+
5274+
$rootScope.$apply('t = false');
5275+
expect(countScopes($rootScope)).toBe(2);
5276+
expect(element.text()).toBe('');
5277+
5278+
$rootScope.$apply('t = true');
5279+
expect(countScopes($rootScope)).toBe(5);
5280+
expect(element.text()).toBe('Success');
5281+
});
5282+
});
5283+
5284+
5285+
it('should preserve the bound scope when using recursive transclusion', function() {
5286+
5287+
directive('recursiveTransclude', valueFn({
5288+
transclude: true,
5289+
template: '<div><lazy-compile><div ng-transclude></div></lazy-compile></div>'
5290+
}));
5291+
5292+
inject(function($compile, $rootScope) {
5293+
element = $compile(
5294+
'<div>' +
5295+
'<div ng-init="outer=true"></div>' +
5296+
'<div toggle="t">' +
5297+
'<div recursive-transclude>' +
5298+
'<span ng-if="outer">Success</span><span ng-if="!outer">Error</span>' +
5299+
'</div>' +
5300+
'</div>' +
5301+
'</div>')($rootScope);
5302+
5303+
$rootScope.$apply('t = false');
5304+
expect(countScopes($rootScope)).toBe(2);
5305+
expect(element.text()).toBe('');
5306+
5307+
$rootScope.$apply('t = true');
5308+
expect(countScopes($rootScope)).toBe(5);
5309+
expect(element.text()).toBe('Success');
5310+
5311+
$rootScope.$apply('t = false');
5312+
expect(countScopes($rootScope)).toBe(2);
5313+
expect(element.text()).toBe('');
5314+
5315+
$rootScope.$apply('t = true');
5316+
expect(countScopes($rootScope)).toBe(5);
5317+
expect(element.text()).toBe('Success');
5318+
});
5319+
});
5320+
});
5321+
5322+
5323+
// see issue https://github.com/angular/angular.js/issues/9095
5324+
describe('removing a transcluded element', function() {
52315325

52325326
beforeEach(module(function() {
52335327
directive('toggle', function() {

0 commit comments

Comments
 (0)