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

fix(ngAnimate): remove event prepare class when skipping animation #16677

Closed

Conversation

swuttich
Copy link

What kind of change does this PR introduce? (Bug fix, feature, docs update, ...)
Bug fix

What is the current behavior? (You can also link to an open issue here)
It can happen that an element keeps the ng-enter-prepare css class (maybe for other events as well) when animation is skipped. This can result for example in unwanted invisible elements in your DOM in case prepare phase is using display:none or opacity:0 to avoid blink during animation.

What is the new behavior (if this is a feature change)?
Prepare class is removed when skipping animation.

Does this PR introduce a breaking change?
No.

Please check if the PR fulfills these requirements

  • The commit message follows our guidelines
  • Fix/Feature: Docs have been added/updated
  • Fix/Feature: Tests have been added; existing tests pass

Other information:

@swuttich swuttich changed the base branch from master to v1.7.x August 28, 2018 22:43
@googlebot
Copy link

Thanks for your pull request. It looks like this may be your first contribution to a Google open source project (if not, look below for help). Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

📝 Please visit https://cla.developers.google.com/ to sign.

Once you've signed (or fixed any issues), please reply here (e.g. I signed it!) and we'll verify it.


What to do if you already signed the CLA

Individual signers
Corporate signers

@googlebot
Copy link

Thanks for your pull request. It looks like this may be your first contribution to a Google open source project (if not, look below for help). Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

📝 Please visit https://cla.developers.google.com/ to sign.

Once you've signed (or fixed any issues), please reply here (e.g. I signed it!) and we'll verify it.


What to do if you already signed the CLA

Individual signers
Corporate signers

@swuttich
Copy link
Author

CLA signed.

@googlebot
Copy link

We found a Contributor License Agreement for you (the sender of this pull request), but were unable to find agreements for all the commit author(s) or Co-authors. If you authored these, maybe you used a different email address in the git commits than was used to sign the CLA (login here to double check)? If these were authored by someone else, then they will need to sign a CLA as well, and confirm that they're okay with these being contributed to Google.
In order to pass this check, please resolve this problem and have the pull request author add another comment and the bot will run again. If the bot doesn't comment, it means it doesn't think anything has changed.

1 similar comment
@googlebot
Copy link

We found a Contributor License Agreement for you (the sender of this pull request), but were unable to find agreements for all the commit author(s) or Co-authors. If you authored these, maybe you used a different email address in the git commits than was used to sign the CLA (login here to double check)? If these were authored by someone else, then they will need to sign a CLA as well, and confirm that they're okay with these being contributed to Google.
In order to pass this check, please resolve this problem and have the pull request author add another comment and the bot will run again. If the bot doesn't comment, it means it doesn't think anything has changed.

@swuttich swuttich force-pushed the missing_removal_prepare_class branch from 51cb9a7 to feb01ef Compare August 28, 2018 23:06
@googlebot
Copy link

CLAs look good, thanks!

1 similar comment
@googlebot
Copy link

CLAs look good, thanks!

@swuttich swuttich changed the base branch from v1.7.x to master August 29, 2018 07:38
@swuttich swuttich force-pushed the missing_removal_prepare_class branch from feb01ef to 5a022bf Compare August 29, 2018 07:38
swuttich referenced this pull request Aug 29, 2018
…imation has no duration

Background:
ngAnimate writes helper classes to DOM elements to see if animations are defined on them. If many
elements have the same definition, and the same parent, we can cache the definition and skip the
application of the helper classes altogether. This helps particularly with large ngRepeat
collections.

Closes #14165
Closes #14166
Closes #16613
@Narretz
Copy link
Contributor

Narretz commented Aug 29, 2018

Thanks for the PR! This looks good at first glance. However, we also need a test for this behavior. Writing tests for ngAnimate isn't the easiest thing, unfortunately. But if you want to give it a shot, here are some tests regarding preparation.

it('should add the preparation class for an enter animation before a parent class-based animation is applied', function() {
module('ngAnimateMock');
inject(function($animate, $compile, $rootScope, $rootElement, $document) {
element = jqLite(
'<div ng-class="{parent:exp}">' +
'<div ng-if="exp">' +
'</div>' +
'</div>'
);
ss.addRule('.ng-enter', 'transition:2s linear all;');
ss.addRule('.parent-add', 'transition:5s linear all;');
$rootElement.append(element);
jqLite($document[0].body).append($rootElement);
$compile(element)($rootScope);
$rootScope.exp = true;
$rootScope.$digest();
var parent = element;
var child = element.find('div');
expect(parent).not.toHaveClass('parent');
expect(parent).toHaveClass('parent-add');
expect(child).not.toHaveClass('ng-enter');
expect(child).toHaveClass('ng-enter-prepare');
$animate.flush();
expect(parent).toHaveClass('parent parent-add parent-add-active');
expect(child).toHaveClass('ng-enter ng-enter-active');
expect(child).not.toHaveClass('ng-enter-prepare');
});
});
it('should avoid adding the ng-enter-prepare method to a parent structural animation that contains child animations', function() {
module('ngAnimateMock');
inject(function($animate, $compile, $rootScope, $rootElement, $document, $$rAF) {
element = jqLite(
'<div ng-animate-children="true">' +
'<div ng-if="parent" class="parent">' +
'<div ng-if="child" class="child">' +
'<div ng-class="{something:true}"></div>' +
'</div>' +
'</div>' +
'</div>'
);
ss.addRule('.ng-enter', 'transition:2s linear all;');
$rootElement.append(element);
jqLite($document[0].body).append($rootElement);
$compile(element)($rootScope);
$rootScope.parent = true;
$rootScope.child = true;
$rootScope.$digest();
var parent = jqLite(element[0].querySelector('.parent'));
var child = jqLite(element[0].querySelector('.child'));
expect(parent).not.toHaveClass('ng-enter-prepare');
expect(child).toHaveClass('ng-enter-prepare');
$$rAF.flush();
expect(parent).not.toHaveClass('ng-enter-prepare');
expect(child).not.toHaveClass('ng-enter-prepare');
});
});
it('should add the preparation class for an enter animation before a parent class-based animation is applied', function() {
module('ngAnimateMock');
inject(function($animate, $compile, $rootScope, $rootElement, $document) {
element = jqLite(
'<div ng-class="{parent:exp}">' +
'<div ng-if="exp">' +
'</div>' +
'</div>'
);
ss.addRule('.ng-enter', 'transition:2s linear all;');
ss.addRule('.parent-add', 'transition:5s linear all;');
$rootElement.append(element);
jqLite($document[0].body).append($rootElement);
$compile(element)($rootScope);
$rootScope.exp = true;
$rootScope.$digest();
var parent = element;
var child = element.find('div');
expect(parent).not.toHaveClass('parent');
expect(parent).toHaveClass('parent-add');
expect(child).not.toHaveClass('ng-enter');
expect(child).toHaveClass('ng-enter-prepare');
$animate.flush();
expect(parent).toHaveClass('parent parent-add parent-add-active');
expect(child).toHaveClass('ng-enter ng-enter-active');
expect(child).not.toHaveClass('ng-enter-prepare');
});
});

It'll also help if you can tell me how exactly the animation is skipped in your case.

@swuttich swuttich force-pushed the missing_removal_prepare_class branch from 5a022bf to 0761de2 Compare August 30, 2018 09:59
@@ -187,7 +187,7 @@ var $$AnimationProvider = ['$animateProvider', /** @this */ function($animatePro
var element = animationEntry.from ? animationEntry.from.element : animationEntry.element;
var extraClasses = options.addClass;
extraClasses = (extraClasses ? (extraClasses + ' ') : '') + NG_ANIMATE_CLASSNAME;
var cacheKey = $$animateCache.cacheKey(node, event, extraClasses, options.removeClass);
var cacheKey = $$animateCache.cacheKey(node, animationEntry.event, extraClasses, options.removeClass);
Copy link
Author

Choose a reason for hiding this comment

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

I'm still unable to isolate the steps which lead to the issue. While trying to find out I managed to get why it leaves on the containsCachedAnimationWithoutDuration() check. I can see that there is a leave and an enter animation getting triggered. Both are in the animation queue, but when code goes through the animations it creates the cache key based on the first event (leave in this case). When it later comes to the actual execution of the animation it first handles the leave, containsCachedAnimationWithoutDuration() returns false as this key is not in the cache yet and then adds it to cache later (with !valid as the transition-duration is null). Then later the same happens for the enter animation, but due to the same cache key as the leave animation containsCachedAnimationWithoutDuration() now finds the existing non valid entry and returns true and the enter is skipped.

Any idea how the duration can be null?

Copy link
Author

Choose a reason for hiding this comment

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

So I managed now to reproduce it

http://plnkr.co/edit/6CsoTul8LBIGBcHfioYJ
(hitting action, when text goes red it means we are into the situation).

So you need a directive and changing ng-switch and changing ng-class as far as I see. No idea how to put that into a test.

Copy link
Contributor

Choose a reason for hiding this comment

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

Thanks, I'll take a look. I take it the custom directive is necessary? replace: true always complicates things.

Copy link
Author

Choose a reason for hiding this comment

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

Any chance that one can trigger the animation events for an element in the tests? Might be simpler than rebuilding the complex structure.

Copy link
Contributor

Choose a reason for hiding this comment

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

There's animationSpec.js, which tests this behavior somewhat in isolation:

they('should remove the preparation class before the $prop-animation starts',
['enter', 'leave', 'move'], function(animationType) {
inject(function($$animation, $rootScope, $$rAF) {
var expectedClassName = 'ng-' + animationType + '-prepare';
var child = jqLite('<div></div>');
element.append(child);
$$animation(element, animationType);
$$animation(child, animationType);
$rootScope.$digest();
expect(element).not.toHaveClass(expectedClassName);
expect(child).toHaveClass(expectedClassName);
$$rAF.flush();
expect(element).not.toHaveClass(expectedClassName);
expect(child).not.toHaveClass(expectedClassName);
});
});

But since the cacheKey is created in animateCss, I'm not sure it's possible to recreate the case here.
Maybe we'll have to build a full integration test including the custom directive first.

Copy link
Contributor

Choose a reason for hiding this comment

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

I managed to trim down your plnkr to what I think is the minimum to reproduce the problem: http://plnkr.co/edit/m5CaegjmdtpR1pUX7zZ8?p=preview I'll try to put this into a test, then it should be easier to see what happens when. Interestingly, the ngClass directive is necessary for the bug to appear.

Any idea how the duration can be null?

ngAnimate is reading possible animation styles from the element during animation. So if you have an element / directive that could technically be animated, it will look on the classes to find animations. If none are found (basically the default), the animation duration is null. Or did you mean null as in null and not 0?

Copy link
Contributor

Choose a reason for hiding this comment

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

I've opened a PR #16681 that simplifies the fix and adds tests

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants