diff --git a/src/jqLite.js b/src/jqLite.js
index 3eee01f507d1..e81ecb755677 100644
--- a/src/jqLite.js
+++ b/src/jqLite.js
@@ -161,6 +161,12 @@ function jqLiteIsTextNode(html) {
return !HTML_REGEXP.test(html);
}
+function jqLiteAcceptsData(node) {
+ // The window object can accept data but has no nodeType
+ // Otherwise we are only interested in elements (1) and documents (9)
+ return !node.nodeType || node.nodeType === 1 || node.nodeType === 9;
+}
+
function jqLiteBuildFragment(html, context) {
var elem, tmp, tag, wrap,
fragment = context.createDocumentFragment(),
@@ -319,7 +325,7 @@ function jqLiteData(element, key, value) {
if (isSetter) {
// set data only on Elements and Documents
- if (element.nodeType === 1 || element.nodeType === 9) {
+ if (jqLiteAcceptsData(element)) {
data[key] = value;
}
} else {
@@ -750,6 +756,11 @@ forEach({
on: function onFn(element, type, fn, unsupported){
if (isDefined(unsupported)) throw jqLiteMinErr('onargs', 'jqLite#on() does not support the `selector` or `eventData` parameters');
+ // Do not add event handlers to non-elements because they will not be cleaned up.
+ if (!jqLiteAcceptsData(element)) {
+ return;
+ }
+
var events = jqLiteExpandoStore(element, 'events'),
handle = jqLiteExpandoStore(element, 'handle');
diff --git a/test/jqLiteSpec.js b/test/jqLiteSpec.js
index ffe02570f59d..da7200fce934 100644
--- a/test/jqLiteSpec.js
+++ b/test/jqLiteSpec.js
@@ -993,6 +993,16 @@ describe('jqLite', function() {
expect(log).toEqual('click on: A;click on: B;');
});
+ it('should not bind to comment or text nodes', function() {
+ var nodes = jqLite('Some text');
+ var someEventHandler = jasmine.createSpy('someEventHandler');
+
+ nodes.on('someEvent', someEventHandler);
+ nodes.triggerHandler('someEvent');
+
+ expect(someEventHandler).not.toHaveBeenCalled();
+ });
+
it('should bind to all events separated by space', function() {
var elm = jqLite(a),
callback = jasmine.createSpy('callback');
diff --git a/test/ng/compileSpec.js b/test/ng/compileSpec.js
index 5de1728a262b..b0de0f63a831 100755
--- a/test/ng/compileSpec.js
+++ b/test/ng/compileSpec.js
@@ -1,5 +1,13 @@
'use strict';
+
+function calcCacheSize() {
+ var size = 0;
+ for(var key in jqLite.cache) { size++; }
+ return size;
+}
+
+
describe('$compile', function() {
var element, directive, $compile, $rootScope;
@@ -158,12 +166,6 @@ describe('$compile', function() {
});
it('should not leak memory when there are top level empty text nodes', function() {
- var calcCacheSize = function() {
- var size = 0;
- forEach(jqLite.cache, function(item, key) { size++; });
- return size;
- };
-
// We compile the contents of element (i.e. not element itself)
// Then delete these contents and check the cache has been reset to zero
@@ -3928,6 +3930,66 @@ describe('$compile', function() {
});
+
+ it('should not leak if two "element" transclusions are on the same element', function() {
+ var calcCacheSize = function() {
+ var size = 0;
+ forEach(jqLite.cache, function(item, key) { size++; });
+ return size;
+ };
+
+ inject(function($compile, $rootScope) {
+ expect(calcCacheSize()).toEqual(0);
+
+ element = $compile('
')($rootScope);
+ expect(calcCacheSize()).toEqual(1);
+
+ $rootScope.$apply('xs = [0,1]');
+ expect(calcCacheSize()).toEqual(2);
+
+ $rootScope.$apply('xs = [0]');
+ expect(calcCacheSize()).toEqual(1);
+
+ $rootScope.$apply('xs = []');
+ expect(calcCacheSize()).toEqual(1);
+
+ element.remove();
+ expect(calcCacheSize()).toEqual(0);
+ });
+ });
+
+
+ it('should not leak if two "element" transclusions are on the same element', function() {
+ var calcCacheSize = function() {
+ var size = 0;
+ forEach(jqLite.cache, function(item, key) { size++; });
+ return size;
+ };
+ inject(function($compile, $rootScope) {
+ expect(calcCacheSize()).toEqual(0);
+ element = $compile('')($rootScope);
+
+ $rootScope.$apply('xs = [0,1]');
+ // At this point we have a bunch of comment placeholders but no real transcluded elements
+ // So the cache only contains the root element's data
+ expect(calcCacheSize()).toEqual(1);
+
+ $rootScope.$apply('val = true');
+ // Now we have two concrete transcluded elements plus some comments so two more cache items
+ expect(calcCacheSize()).toEqual(3);
+
+ $rootScope.$apply('val = false');
+ // Once again we only have comments so no transcluded elements and the cache is back to just
+ // the root element
+ expect(calcCacheSize()).toEqual(1);
+
+ element.remove();
+ // Now we've even removed the root element along with its cache
+ expect(calcCacheSize()).toEqual(0);
+ });
+ });
+
+
it('should remove transclusion scope, when the DOM is destroyed', function() {
module(function() {
directive('box', valueFn({
@@ -4409,6 +4471,35 @@ describe('$compile', function() {
$rootScope.$digest();
expect(element.text()).toEqual('transcluded content');
}));
+
+
+ it('should not leak memory with nested transclusion', function() {
+ var calcCacheSize = function() {
+ var count = 0;
+ for (var k in jqLite.cache) { ++count; }
+ return count;
+ };
+
+ inject(function($compile, $rootScope) {
+ var size;
+
+ expect(calcCacheSize()).toEqual(0);
+
+ element = jqLite('');
+ $compile(element)($rootScope.$new());
+
+ $rootScope.nums = [0,1,2];
+ $rootScope.$apply();
+ size = calcCacheSize();
+
+ $rootScope.nums = [3,4,5];
+ $rootScope.$apply();
+ expect(calcCacheSize()).toEqual(size);
+
+ element.remove();
+ expect(calcCacheSize()).toEqual(0);
+ });
+ });
});
@@ -5380,7 +5471,7 @@ describe('$compile', function() {
}));
- it('should group on nested groups of same directive', inject(function($compile, $rootScope) {
+ it('should group on nested groups', inject(function($compile, $rootScope) {
$rootScope.show = false;
element = $compile(
'' +
@@ -5395,7 +5486,7 @@ describe('$compile', function() {
}));
- it('should group on nested groups', inject(function($compile, $rootScope) {
+ it('should group on nested groups of same directive', inject(function($compile, $rootScope) {
$rootScope.show = false;
element = $compile(
'' +
@@ -5410,6 +5501,176 @@ describe('$compile', function() {
}));
+ it('should set up and destroy the transclusion scopes correctly',
+ inject(function($compile, $rootScope) {
+ element = $compile(
+ ''
+ )($rootScope);
+ $rootScope.$apply('val0 = true; val1 = true; val2 = true');
+
+ // At this point we should have something like:
+ //
+ //
+ //
+ //
+ //
+ //
+ //
+ //
+ //
+ //
+ //
+ //
+ //
+ //
+ //
+ //
+ //
+ //
+ //
+ var ngIfStartScope = element.find('div').eq(0).scope();
+ var ngIfEndScope = element.find('div').eq(1).scope();
+
+ expect(ngIfStartScope.$id).toEqual(ngIfEndScope.$id);
+
+ var ngIf1Scope = element.find('span').eq(0).scope();
+ var ngIf2Scope = element.find('span').eq(1).scope();
+
+ expect(ngIf1Scope.$id).not.toEqual(ngIf2Scope.$id);
+ expect(ngIf1Scope.$parent.$id).toEqual(ngIf2Scope.$parent.$id);
+
+ $rootScope.$apply('val1 = false');
+
+ // Now we should have something like:
+ //
+ //
+ //
+ //
+ //
+ //
+ //
+ //
+ //
+ //
+ //
+ //
+ //
+
+ expect(ngIfStartScope.$$destroyed).not.toEqual(true);
+ expect(ngIf1Scope.$$destroyed).toEqual(true);
+ expect(ngIf2Scope.$$destroyed).not.toEqual(true);
+
+ $rootScope.$apply('val0 = false');
+
+ // Now we should have something like:
+ //
+ //
+ //
+ //
+
+ expect(ngIfStartScope.$$destroyed).toEqual(true);
+ expect(ngIf1Scope.$$destroyed).toEqual(true);
+ expect(ngIf2Scope.$$destroyed).toEqual(true);
+ }));
+
+
+ it('should set up and destroy the transclusion scopes correctly',
+ inject(function($compile, $rootScope) {
+ element = $compile(
+ ''
+ )($rootScope);
+
+ // To begin with there is (almost) nothing:
+ //
+ //
+ //
+
+ expect(element.scope().$id).toEqual($rootScope.$id);
+
+ // Now we create all the elements
+ $rootScope.$apply('val0 = [1]; val1 = true; val2 = true');
+
+ // At this point we have:
+ //
+ //
+ //
+ //
+ //
+ //
+ //
+ //
+ //
+ //
+ //
+ //
+ //
+ //
+ //
+ var ngIf1Scope = element.find('div').eq(0).scope();
+ var ngIf2Scope = element.find('div').eq(1).scope();
+ var ngRepeatScope = ngIf1Scope.$parent;
+
+ expect(ngIf1Scope.$id).not.toEqual(ngIf2Scope.$id);
+ expect(ngIf1Scope.$parent.$id).toEqual(ngRepeatScope.$id);
+ expect(ngIf2Scope.$parent.$id).toEqual(ngRepeatScope.$id);
+
+ // What is happening here??
+ // We seem to have a repeater scope which doesn't actually match to any element
+ expect(ngRepeatScope.$parent.$id).toEqual($rootScope.$id);
+
+
+ // Now remove the first ngIf element from the first item in the repeater
+ $rootScope.$apply('val1 = false');
+
+ // At this point we should have:
+ //
+ //
+ //
+ //
+ //
+ //
+ //
+ //
+ //
+ //
+ //
+ //
+ //
+ expect(ngRepeatScope.$$destroyed).toEqual(false);
+ expect(ngIf1Scope.$$destroyed).toEqual(true);
+ expect(ngIf2Scope.$$destroyed).toEqual(false);
+
+ // Now remove the second ngIf element from the first item in the repeater
+ $rootScope.$apply('val2 = false');
+
+ // We are mostly back to where we started
+ //
+ //
+ //
+ //
+ //
+ //
+ //
+
+ expect(ngRepeatScope.$$destroyed).toEqual(false);
+ expect(ngIf1Scope.$$destroyed).toEqual(true);
+ expect(ngIf2Scope.$$destroyed).toEqual(true);
+
+ // Finally remove the repeat items
+ $rootScope.$apply('val0 = []');
+
+ // Somehow this ngRepeat scope knows how to destroy itself...
+ expect(ngRepeatScope.$$destroyed).toEqual(true);
+ expect(ngIf1Scope.$$destroyed).toEqual(true);
+ expect(ngIf2Scope.$$destroyed).toEqual(true);
+ }));
+
it('should throw error if unterminated', function () {
module(function($compileProvider) {
$compileProvider.directive('foo', function() {