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

Commit d71dbb1

Browse files
committed
refactor(jqLite): stop patching individual jQuery methods
Currently Angular monkey-patches a few jQuery methods that remove elements from the DOM. Since methods like .remove() have multiple signatures that can change what's actually removed, Angular needs to carefully repeat them in its patching or it can break apps using jQuery correctly. Such a strategy is also not future-safe. Instead of patching individual methods on the prototype, it's better to hook into jQuery.cleanData and trigger custom events there. This should be safe as e.g. jQuery UI needs it and uses it. It'll also be future-safe. The only drawback is that $destroy is no longer triggered when using $detach but: 1. Angular doesn't use this method, jqLite doesn't implement it. 2. Detached elements can be re-attached keeping all their events & data so it makes sense that $destroy is not triggered on them. 3. The approach from this commit is so much safer that any issues with .detach() working differently are outweighed by the robustness of the code. BREAKING CHANGE: the $destroy event is no longer triggered when using the jQuery detach() method. If you want to destroy Angular data attached to the element, use remove().
1 parent be7c02c commit d71dbb1

File tree

3 files changed

+15
-58
lines changed

3 files changed

+15
-58
lines changed

src/Angular.js

+15-5
Original file line numberDiff line numberDiff line change
@@ -1430,8 +1430,10 @@ function snake_case(name, separator){
14301430
}
14311431

14321432
function bindJQuery() {
1433+
var originalCleanData;
14331434
// bind to jQuery if present;
14341435
jQuery = window.jQuery;
1436+
14351437
// reset to jQuery or default to us.
14361438
if (jQuery) {
14371439
jqLite = jQuery;
@@ -1442,14 +1444,22 @@ function bindJQuery() {
14421444
injector: JQLitePrototype.injector,
14431445
inheritedData: JQLitePrototype.inheritedData
14441446
});
1445-
// Method signature:
1446-
// jqLitePatchJQueryRemove(name, dispatchThis, filterElems, getterIfNoArguments)
1447-
jqLitePatchJQueryRemove('remove', true, true, false);
1448-
jqLitePatchJQueryRemove('empty', false, false, false);
1449-
jqLitePatchJQueryRemove('html', false, false, true);
1447+
1448+
originalCleanData = jQuery.cleanData;
1449+
// Prevent double-proxying.
1450+
originalCleanData = originalCleanData.$$original || originalCleanData;
1451+
1452+
jQuery.cleanData = function(elems) {
1453+
for (var i = 0, elem; (elem = elems[i]) != null; i++) {
1454+
jQuery(elem).triggerHandler('$destroy');
1455+
}
1456+
originalCleanData(elems);
1457+
};
1458+
jQuery.cleanData.$$original = originalCleanData;
14501459
} else {
14511460
jqLite = JQLite;
14521461
}
1462+
14531463
angular.element = jqLite;
14541464
}
14551465

src/jqLite.js

-43
Original file line numberDiff line numberDiff line change
@@ -136,49 +136,6 @@ function camelCase(name) {
136136
replace(MOZ_HACK_REGEXP, 'Moz$1');
137137
}
138138

139-
/////////////////////////////////////////////
140-
// jQuery mutation patch
141-
//
142-
// In conjunction with bindJQuery intercepts all jQuery's DOM destruction apis and fires a
143-
// $destroy event on all DOM nodes being removed.
144-
//
145-
/////////////////////////////////////////////
146-
147-
function jqLitePatchJQueryRemove(name, dispatchThis, filterElems, getterIfNoArguments) {
148-
var originalJqFn = jQuery.fn[name];
149-
originalJqFn = originalJqFn.$original || originalJqFn;
150-
removePatch.$original = originalJqFn;
151-
jQuery.fn[name] = removePatch;
152-
153-
function removePatch(param) {
154-
// jshint -W040
155-
var list = filterElems && param ? [this.filter(param)] : [this],
156-
fireEvent = dispatchThis,
157-
set, setIndex, setLength,
158-
element, childIndex, childLength, children;
159-
160-
if (!getterIfNoArguments || param != null) {
161-
while(list.length) {
162-
set = list.shift();
163-
for(setIndex = 0, setLength = set.length; setIndex < setLength; setIndex++) {
164-
element = jqLite(set[setIndex]);
165-
if (fireEvent) {
166-
element.triggerHandler('$destroy');
167-
} else {
168-
fireEvent = !fireEvent;
169-
}
170-
for(childIndex = 0, childLength = (children = element.children()).length;
171-
childIndex < childLength;
172-
childIndex++) {
173-
list.push(jQuery(children[childIndex]));
174-
}
175-
}
176-
}
177-
}
178-
return originalJqFn.apply(this, arguments);
179-
}
180-
}
181-
182139
var SINGLE_TAG_REGEXP = /^<(\w+)\s*\/?>(?:<\/\1>|)$/;
183140
var HTML_REGEXP = /<|&#?\w+;/;
184141
var TAG_NAME_REGEXP = /<([\w:]+)/;

test/jQueryPatchSpec.js

-10
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,6 @@ if (window.jQuery) {
3030

3131
describe('$detach event', function() {
3232

33-
it('should fire on detach()', function() {
34-
doc.find('span').detach();
35-
});
36-
3733
it('should fire on remove()', function() {
3834
doc.find('span').remove();
3935
});
@@ -83,12 +79,6 @@ if (window.jQuery) {
8379

8480
describe('$detach event is not invoked in too many cases', function() {
8581

86-
it('should fire only on matched elements on detach(selector)', function() {
87-
doc.find('span').detach('.second');
88-
expect(spy2).toHaveBeenCalled();
89-
expect(spy2.callCount).toEqual(1);
90-
});
91-
9282
it('should fire only on matched elements on remove(selector)', function() {
9383
doc.find('span').remove('.second');
9484
expect(spy2).toHaveBeenCalled();

0 commit comments

Comments
 (0)