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

Commit b9389b2

Browse files
committed
fix(jQuery): cooperate with other libraries monkey-patching jQuery.cleanData
Some libraries (like jQuery UI) patch jQuery.cleanData as well. This commit makes Angular work correctly even if such external patching was done after the Angular one. Fixes #8471
1 parent 6bdaa4b commit b9389b2

File tree

5 files changed

+79
-27
lines changed

5 files changed

+79
-27
lines changed

src/.jshintrc

+2
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,8 @@
8888
"getBlockElements": false,
8989
"VALIDITY_STATE_PROPERTY": false,
9090

91+
"skipDestroyOnNextJQueryCleanData": true,
92+
9193
/* filters.js */
9294
"getFirstThursdayOfYear": false,
9395

src/Angular.js

+17-7
Original file line numberDiff line numberDiff line change
@@ -1454,8 +1454,15 @@ function snake_case(name, separator) {
14541454
});
14551455
}
14561456

1457+
var bindJQueryFired = false;
1458+
var skipDestroyOnNextJQueryCleanData;
14571459
function bindJQuery() {
14581460
var originalCleanData;
1461+
1462+
if (bindJQueryFired) {
1463+
return;
1464+
}
1465+
14591466
// bind to jQuery if present;
14601467
jQuery = window.jQuery;
14611468
// Use jQuery if it exists with proper functionality, otherwise default to us.
@@ -1472,25 +1479,28 @@ function bindJQuery() {
14721479
inheritedData: JQLitePrototype.inheritedData
14731480
});
14741481

1475-
originalCleanData = jQuery.cleanData;
1476-
// Prevent double-proxying.
1477-
originalCleanData = originalCleanData.$$original || originalCleanData;
1478-
14791482
// All nodes removed from the DOM via various jQuery APIs like .remove()
14801483
// are passed through jQuery.cleanData. Monkey-patch this method to fire
14811484
// the $destroy event on all removed nodes.
1485+
originalCleanData = jQuery.cleanData;
14821486
jQuery.cleanData = function(elems) {
1483-
for (var i = 0, elem; (elem = elems[i]) != null; i++) {
1484-
jQuery(elem).triggerHandler('$destroy');
1487+
if (!skipDestroyOnNextJQueryCleanData) {
1488+
for (var i = 0, elem; (elem = elems[i]) != null; i++) {
1489+
jQuery(elem).triggerHandler('$destroy');
1490+
}
1491+
} else {
1492+
skipDestroyOnNextJQueryCleanData = false;
14851493
}
14861494
originalCleanData(elems);
14871495
};
1488-
jQuery.cleanData.$$original = originalCleanData;
14891496
} else {
14901497
jqLite = JQLite;
14911498
}
14921499

14931500
angular.element = jqLite;
1501+
1502+
// Prevent double-proxying.
1503+
bindJQueryFired = true;
14941504
}
14951505

14961506
/**

src/ng/compile.js

+9-5
Original file line numberDiff line numberDiff line change
@@ -2043,11 +2043,15 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
20432043
if (!jQuery) {
20442044
delete jqLite.cache[firstElementToRemove[jqLite.expando]];
20452045
} else {
2046-
// jQuery 2.x doesn't expose the data storage. Use jQuery.cleanData to clean up after the replaced
2047-
// element. Note that we need to use the original method here and not the one monkey-patched by Angular
2048-
// since the patched method emits the $destroy event causing the scope to be trashed and we do need
2049-
// the very same scope to work with the new element.
2050-
jQuery.cleanData.$$original([firstElementToRemove]);
2046+
// jQuery 2.x doesn't expose the data storage. Use jQuery.cleanData to clean up after
2047+
// the replaced element. The cleanData version monkey-patched by Angular would cause
2048+
// the scope to be trashed and we do need the very same scope to work with the new
2049+
// element. However, we cannot just cache the non-patched version and use it here as
2050+
// that would break if another library patches the method after Angular does (one
2051+
// example is jQuery UI). Instead, set a flag indicating scope destroying should be
2052+
// skipped this one time.
2053+
skipDestroyOnNextJQueryCleanData = true;
2054+
jQuery.cleanData([firstElementToRemove]);
20512055
}
20522056

20532057
for (var k = 1, kk = elementsToRemove.length; k < kk; k++) {

test/helpers/testabilityPatch.js

+5
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,11 @@ function dealoc(obj) {
112112

113113
function cleanup(element) {
114114
element.off().removeData();
115+
if (window.jQuery) {
116+
// jQuery 2.x doesn't expose the cache storage; ensure all element data
117+
// is removed during its cleanup.
118+
jQuery.cleanData([element]);
119+
}
115120
// Note: We aren't using element.contents() here. Under jQuery, element.contents() can fail
116121
// for IFRAME elements. jQuery explicitly uses (element.contentDocument ||
117122
// element.contentWindow.document) and both properties are null for IFRAMES that aren't attached

test/ng/compileSpec.js

+46-15
Original file line numberDiff line numberDiff line change
@@ -4118,26 +4118,57 @@ describe('$compile', function() {
41184118
});
41194119

41204120
if (jQuery) {
4121-
it('should clean up after a replaced element', inject(function ($compile) {
4122-
var privateData, firstRepeatedElem;
4121+
describe('cleaning up after a replaced element', function () {
4122+
var $compile, xs;
4123+
beforeEach(inject(function (_$compile_) {
4124+
$compile = _$compile_;
4125+
xs = [0, 1];
4126+
}));
41234127

4124-
element = $compile('<div><div ng-repeat="x in xs">{{x}}</div></div>')($rootScope);
4128+
function testCleanup() {
4129+
var privateData, firstRepeatedElem;
41254130

4126-
$rootScope.$apply('xs = [0,1]');
4127-
firstRepeatedElem = element.children('.ng-scope').eq(0);
4131+
element = $compile('<div><div ng-repeat="x in xs">{{x}}</div></div>')($rootScope);
41284132

4129-
expect(firstRepeatedElem.data('$scope')).toBeDefined();
4130-
privateData = jQuery._data(firstRepeatedElem[0]);
4131-
expect(privateData.events).toBeDefined();
4132-
expect(privateData.events.$destroy).toBeDefined();
4133-
expect(privateData.events.$destroy[0]).toBeDefined();
4133+
$rootScope.$apply('xs = [' + xs + ']');
4134+
firstRepeatedElem = element.children('.ng-scope').eq(0);
41344135

4135-
$rootScope.$apply('xs = null');
4136+
expect(firstRepeatedElem.data('$scope')).toBeDefined();
4137+
privateData = jQuery._data(firstRepeatedElem[0]);
4138+
expect(privateData.events).toBeDefined();
4139+
expect(privateData.events.$destroy).toBeDefined();
4140+
expect(privateData.events.$destroy[0]).toBeDefined();
41364141

4137-
expect(firstRepeatedElem.data('$scope')).not.toBeDefined();
4138-
privateData = jQuery._data(firstRepeatedElem[0]);
4139-
expect(privateData && privateData.events).not.toBeDefined();
4140-
}));
4142+
$rootScope.$apply('xs = null');
4143+
4144+
expect(firstRepeatedElem.data('$scope')).not.toBeDefined();
4145+
privateData = jQuery._data(firstRepeatedElem[0]);
4146+
expect(privateData && privateData.events).not.toBeDefined();
4147+
}
4148+
4149+
it('should work without external libraries (except jQuery)', testCleanup);
4150+
4151+
it('should work with another library patching jQuery.cleanData after Angular', function () {
4152+
var cleanedCount = 0;
4153+
var currentCleanData = jQuery.cleanData;
4154+
jQuery.cleanData = function (elems) {
4155+
cleanedCount += elems.length;
4156+
// Don't return the output and expicitly pass only the first parameter
4157+
// so that we're sure we're not relying on either of them. jQuery UI patch
4158+
// behaves in this way.
4159+
currentCleanData(elems);
4160+
};
4161+
4162+
testCleanup();
4163+
4164+
// The initial ng-repeat div is dumped after parsing hence we expect cleanData
4165+
// count to be one larger than size of the iterated array.
4166+
expect(cleanedCount).toBe(xs.length + 1);
4167+
4168+
// Restore the previous jQuery.cleanData.
4169+
jQuery.cleanData = currentCleanData;
4170+
});
4171+
});
41414172
}
41424173

41434174

0 commit comments

Comments
 (0)