-
Notifications
You must be signed in to change notification settings - Fork 27.4k
fix(ngAnimate): defer DOM operations for changing classes to postDigest #9283
Conversation
So I personally think the other fix is tinier and able to work for local digests, so that's kind of a good thing... this obviously is basically taking a bunch of meat out of ngAnimate and moving it to core, so it grows the code maybe more than it should. But both solutions appear to fix the jank, so yay. It's possible that this could be reworked better to have less code duplication between ng and ngAnimate. /cc @matsko |
I think we need more tests for this change. Could you copy the corresponding tests from Also, as |
will do |
The way this is organized, it will be a bit of effort to remove code duplication in a way that doesn't break one module or the other --- I want to do this and was hoping Matias could help, but it might not get done for this release. I don't think this should block landing it |
@@ -100,4 +100,118 @@ describe("$animate", function() { | |||
inject(); | |||
}); | |||
}); | |||
|
|||
describe('class API', function() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ping @tbosch --- I didn't see any similar tests to this in ngAnimate, so we might want to move these there too. Let me know if these look good to you
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As ngAnimate
decorates $animate
from core, the functionality should only be in core, and the tests for this as well. Looking at the tests...
... I think the toHaveClass matchers don't really work correctly for SVG :( |
}); | ||
|
||
it('should defer class manipulation until end of digest', inject(function($rootScope, $animate) { | ||
element = jqLite('<p>test</p>'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you add an initial class that is removed during the digest and check that it is not really removed until the end of the digest?
8932480
to
dd7691c
Compare
@caitp After our talk in the meeting, please make the following changes:
|
return this.setClass(element, className, []); | ||
}, | ||
|
||
$$addClass : function(element, className) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we rename this to $$addClassImmediately
?
For the tests, you can actually spy on |
|
I've added a bunch of new tests, I am sure I could come up with 50 more if I need to, but I'm not totally sure what I'm verifying (particularly in the promise case). How does this look? |
So, since functionality is not totally shared between the two, copy tests into ngAnimate? Is the promise functionality being tested enough for you here? Is the ordering tested enough? Does this look alright? PTAL --- If it looks good, will fix the commit message to include a breaking change notice and land |
Tests look good. For the promise case this is enough (just to make sure the promise is actually resolved...). Re breaking change: Yes, the fact that classes are not always being added immediately any more. Yes, good idea, let's add the tests also to |
yep --- mentioned in the chat, I'll try to have this updated tonight or early tomorrow |
Okay --- everything should be addressed now. PTAL |
var createdCache = false; | ||
element = jqLite(element); | ||
|
||
if (runSynchronously) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unfortunately, ngClass
has a test which fails if core ngAnimate doesn't work synchronously. I spent some time trying to fix the test, but I couldn't really decide how it should work.
This is bad because it's a definite difference in behaviour for one special case, so I think we'll want to fix this --- but maybe it can wait a bit?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
obviously, runSynchronously
is not documented since I think we should get rid of it within a week or two, but I don't think it should block landing the CL
When ngAnimate is used, it will defer changes to classes until postDigest. Previously, AngularJS (when ngAnimate is not loaded) would always immediately perform these DOM operations. Now, even when the ngAnimate module is not used, if $rootScope is in the midst of a digest, class manipulation is deferred. This helps reduce jank in browsers such as IE11. Closes angular#8234 Closes angular#9263
Always defer class manipulation DOM writes until the end of digest on the root scope, even when animations are disabled. BREAKING CHANGE: The $animate class API will always defer changes until the end of the next digest. This allows ngAnimate to coalesce class changes which occur over a short period of time into 1 or 2 DOM writes, rather than many. This prevents jank in browsers such as IE, and is generally a good thing. If you're finding that your classes are not being immediately applied, be sure to invoke $digest().
So can we get one last look at this before the release? I'd like to get it checked in. As stated, there is some rubbish in there that I think we can get rid of subsequently, but I don't think it should block landing this (it could have been fixed, but I just wasn't sure how the tests should have been reworked because of it). I'm also not positive all of the changes in the last commit are really that useful, but they were asked for. Can revert parts of it if needed |
function resolveElementClasses(element, cache) { | ||
var toAdd = [], toRemove = []; | ||
forEach(cache.classes, function(status, className) { | ||
var hasClass = jqLiteHasClass(element[0], className); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this will have terrible perf when multiple classes are added. can we read the existing classes from the dom only once for render?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I mentioned on Matsko's PR that we don't really need to care if an element has the class already or not --- it could improve perf to just skip reading it. It's not very nice for tests though
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure what you mean by "read the existing classes from the dom only once for render".
Do you mean like just do `hasClass = (' ' + element.className + ' ').indexOf(' ' + className + ' ') >= 0)`` or something?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(where the class attribute would be cached)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
just read out the className, split it, create a map and use that map for checking if class is already in the dom
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done, PTAL
@@ -979,7 +979,7 @@ angular.module('ngAnimate', ['ng']) | |||
element = stripCommentsFromElement(element); | |||
|
|||
if (classBasedAnimationsBlocked(element)) { | |||
return $delegate.setClass(element, add, remove); | |||
return $delegate.setClass(element, add, remove, true); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can't we use jqLite here instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oh, we need to return the promise which complicates things
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we do, when jQuery is used we'll die on SVG elements
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I do want to fix this up so that it sucks less though (but I think matsko will need to help fix broken tests that fail when we don't do this synchronously)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok. let's keep it as is for now. can we add a comment that we call it this way via undocumented api.
This also does some cleanup in $animate
…ementClasses In ngAnimate, we can't do this because the behaviour is exposed via the API. But in core, we can avoid a bit of work.
Previously we were reading DOM attributes frequently, now we can do it just once.
It was previously used for ngAnimate, but is no longer needed
}); | ||
|
||
forEach(cache.classes, function(status, className) { | ||
var hasClass = hasClasses[className] === true; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
=== true
so that classes like toString
or constructor
don't cause problems
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
use createMap
instead and then you don't need this
abdaab7
to
30996f8
Compare
When ngAnimate is used, it will defer changes to classes until postDigest.
Previously, AngularJS (when ngAnimate is not loaded) would always immediately
perform these DOM operations.
Now, even when the ngAnimate module is not used, if $rootScope is in the midst
of a digest, class manipulation is deferred. This helps reduce jank in
browsers such as IE11.
/CC @tbosch --- alternative/more general fix here
Closes #8234
Closes #9263