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

Commit

Permalink
refactor(jqLite): stop patching individual jQuery methods
Browse files Browse the repository at this point in the history
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().
  • Loading branch information
mgol committed May 10, 2014
1 parent be7c02c commit d71dbb1
Show file tree
Hide file tree
Showing 3 changed files with 15 additions and 58 deletions.
20 changes: 15 additions & 5 deletions src/Angular.js
Original file line number Diff line number Diff line change
Expand Up @@ -1430,8 +1430,10 @@ function snake_case(name, separator){
}

function bindJQuery() {
var originalCleanData;
// bind to jQuery if present;
jQuery = window.jQuery;

// reset to jQuery or default to us.
if (jQuery) {
jqLite = jQuery;
Expand All @@ -1442,14 +1444,22 @@ function bindJQuery() {
injector: JQLitePrototype.injector,
inheritedData: JQLitePrototype.inheritedData
});
// Method signature:
// jqLitePatchJQueryRemove(name, dispatchThis, filterElems, getterIfNoArguments)
jqLitePatchJQueryRemove('remove', true, true, false);
jqLitePatchJQueryRemove('empty', false, false, false);
jqLitePatchJQueryRemove('html', false, false, true);

originalCleanData = jQuery.cleanData;
// Prevent double-proxying.
originalCleanData = originalCleanData.$$original || originalCleanData;

jQuery.cleanData = function(elems) {
for (var i = 0, elem; (elem = elems[i]) != null; i++) {
jQuery(elem).triggerHandler('$destroy');
}
originalCleanData(elems);
};
jQuery.cleanData.$$original = originalCleanData;
} else {
jqLite = JQLite;
}

angular.element = jqLite;
}

Expand Down
43 changes: 0 additions & 43 deletions src/jqLite.js
Original file line number Diff line number Diff line change
Expand Up @@ -136,49 +136,6 @@ function camelCase(name) {
replace(MOZ_HACK_REGEXP, 'Moz$1');
}

/////////////////////////////////////////////
// jQuery mutation patch
//
// In conjunction with bindJQuery intercepts all jQuery's DOM destruction apis and fires a
// $destroy event on all DOM nodes being removed.
//
/////////////////////////////////////////////

function jqLitePatchJQueryRemove(name, dispatchThis, filterElems, getterIfNoArguments) {
var originalJqFn = jQuery.fn[name];
originalJqFn = originalJqFn.$original || originalJqFn;
removePatch.$original = originalJqFn;
jQuery.fn[name] = removePatch;

function removePatch(param) {
// jshint -W040
var list = filterElems && param ? [this.filter(param)] : [this],
fireEvent = dispatchThis,
set, setIndex, setLength,
element, childIndex, childLength, children;

if (!getterIfNoArguments || param != null) {
while(list.length) {
set = list.shift();
for(setIndex = 0, setLength = set.length; setIndex < setLength; setIndex++) {
element = jqLite(set[setIndex]);
if (fireEvent) {
element.triggerHandler('$destroy');
} else {
fireEvent = !fireEvent;
}
for(childIndex = 0, childLength = (children = element.children()).length;
childIndex < childLength;
childIndex++) {
list.push(jQuery(children[childIndex]));
}
}
}
}
return originalJqFn.apply(this, arguments);
}
}

var SINGLE_TAG_REGEXP = /^<(\w+)\s*\/?>(?:<\/\1>|)$/;
var HTML_REGEXP = /<|&#?\w+;/;
var TAG_NAME_REGEXP = /<([\w:]+)/;
Expand Down
10 changes: 0 additions & 10 deletions test/jQueryPatchSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,6 @@ if (window.jQuery) {

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

it('should fire on detach()', function() {
doc.find('span').detach();
});

it('should fire on remove()', function() {
doc.find('span').remove();
});
Expand Down Expand Up @@ -83,12 +79,6 @@ if (window.jQuery) {

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

it('should fire only on matched elements on detach(selector)', function() {
doc.find('span').detach('.second');
expect(spy2).toHaveBeenCalled();
expect(spy2.callCount).toEqual(1);
});

it('should fire only on matched elements on remove(selector)', function() {
doc.find('span').remove('.second');
expect(spy2).toHaveBeenCalled();
Expand Down

15 comments on commit d71dbb1

@dmaj7no5th
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this be ported to the 1.2x branch? The current monkeypatching in that branch is giving me problems with jquery 1.8.x on IE9

@mgol
Copy link
Member Author

@mgol mgol commented on d71dbb1 Jun 9, 2014

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dmaj7no5th What problems, can you report them? @IgorMinar would have to decide but we generally don't backport refactorings that aren't bug fixes. If it indeed fixes a bug you experience, this may be an incentive.

You can try to cherry-pick this commit onto the v1.2.x branch, create a package via npm i && grunt and see if it helps. Such info would be useful.

@dmaj7no5th
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Specifically, using ngRoute. On route changes, when ngRoute cleans up the old view, it calls element.remove(). Somewhere within the mutation patch above (removed in this commit), IE9 throws a "Access is Denied" error. Using angular 1.2.16 with jQuery 1.8.3. Commenting out the monkeypatch makes the routeChange work without error.

I'll see if I can reproduce in a clean app and report an issue

@IgorMinar
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you upgrade to jquery 1.10.x? We don't test against 1.8.3.

@mgol
Copy link
Member Author

@mgol mgol commented on d71dbb1 Jun 10, 2014

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@IgorMinar

can you upgrade to jquery 1.10.x? We don't test against 1.8.3.

That's true, though we officialy state jQuery 1.7 or higher is required and I don't think we can just say we support only the latest version since not all projects can update in this pace.

Also, why 1.10.x and not 1.11.1?

@IgorMinar
Copy link
Contributor

@IgorMinar IgorMinar commented on d71dbb1 Jun 10, 2014 via email

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mgol
Copy link
Member Author

@mgol mgol commented on d71dbb1 Jun 10, 2014

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dmaj7no5th Doesn't IE9 print any specific line where the problem arises? That would make it easier to figure out. Also, just tu make sure: are you running your app via a local file server and not just via opening static HTML files?

@dmaj7no5th
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Upgrading jQuery doesn't fix the problem. After some more debugging it seems the problem goes away if I disable my CSS. I'm not using the ngAnimate module, nor do I have any CSS transitions enabled - but it appears something's going on with the CSS that's breaking ngRoute somehow, and due to that, IE9 won't let jQuery remove the DOM elements.

@mgol
Copy link
Member Author

@mgol mgol commented on d71dbb1 Jun 10, 2014 via email

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dmaj7no5th
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The debugger doesn't give me any line numbers. I've been trying different settings in Internet Options, but all I can get out of it is "Permission Denied." Once the error occurrs, I have 2 ng-view divs in the DOM.
$('[ng-view]').remove() will throw the same "Permission Denied" error again and again once it occurs.
I'm grasping at straws with the debugger to find exactly what line it's happening on without much luck

@rwlogel
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was any decision made about porting this into the 1.2.x branch? I have a directive that uses detach to relocate an element in the DOM and it worked in version 1.2.12, before the $destroy method was corrected. If this change could be ported into 1.2.x and the detach() didn't trigger the $destroy at all it would work again.

@mgol
Copy link
Member Author

@mgol mgol commented on d71dbb1 Jul 31, 2014

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@IgorMinar I think we should backport this fix to 1.2.x. This is technically a breaking change but the current behavior is just a bug as it doesn't align with the jQuery meaning of detach. There seem to be enough people affected by past tightening up leaks that hit .detach() that it IMO makes sense to fix them with this patch.

An alternative approach would be to just modify the jQuery patch in Angular 1.2 to not trigger $destroy when using .detach() without switching to the cleanData cleaner way from Angular 1.3.

Anyway, IMO we should do one of these things. I prefer the former but it's your call.

@rwlogel
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that a simpler patch might be appropriate, in the jqLitePatchJQueryRemove function if the removePatch(param) method was changed to removePatch(param, keepData) and keepData was used to either exit the function immediately or just prevent the the $destroy from triggering that should fix it.

@mgol
Copy link
Member Author

@mgol mgol commented on d71dbb1 Jul 31, 2014

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rwlogel Care to submit a PR? :) Link to it from here.

Maybe it's indeed a better way to not change the whole patch in a patch release if a simpler fix exists.

@mgol
Copy link
Member Author

@mgol mgol commented on d71dbb1 Aug 13, 2014

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rwlogel Do you want to tackle it?

Please sign in to comment.