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

$animateProvider.classNameFilter causes ng-hide animation to play on element load #15426

Closed
mike-bresnahan opened this issue Nov 23, 2016 · 10 comments

Comments

@mike-bresnahan
Copy link

mike-bresnahan commented Nov 23, 2016

Upon upgrading from AngularJS 1.3 to 1.5, I think I have encountered a bug in ngAnimate.

When:

  1. $animateProvider.classNameFilter is used to filter the elements that are to be animated,
  2. the element has a ng-show that is set to false initially

Upon load of the element, the ng-hide animation is played.

In 1.3 the ng-hide animation did not play and if I remove the call to $animateProvider.classNameFilter the animation does not play, so I do not expect the animation to play in the case above. Instead I expect the element to be hidden upon load.

A plnkr is here:

https://plnkr.co/edit/PwmgrB8X8SRHpOpNYnFb

Click on "Toggle Container Visibility" and observe that a sliding animation is played. Next, comment out $animateProvider.classNameFilter(/animate-/); in script.js and observe that the animation does not play.

@Narretz
Copy link
Contributor

Narretz commented Nov 24, 2016

Yeah, that's odd. Although I would expect the animation to play in both cases, with and without filter. Because you clearly have an animation defined.
Although we do make an exception on app bootstrap, where no animation are played with the first two digests (I think).
I think this change was intentional, as one big thing fixed in 1.4 was to allow child animations to run when the parent element (possibly) has an animation.

@gkalpak
Copy link
Member

gkalpak commented Nov 24, 2016

This is (kind of) expected behavior. I am not sure about 1.3, but in 1.5.x a class-based animation is (by default) disabed if there is structural (enter, leave, move) animation going on on a parent element.

When you are not using classNameFilter, the ngIf causes an enter animation to be initiated (even if there is no actual animation duration, so the animation will end immediately - but still asynchronously). At the time when the ngShow (class-based) animation is being processed, there is a structural animation on a parent element, so the class-based one is disabled (i.e. the class is added immediately, without animating it).

When using classNameFilter, since the parent div (the one with the ngIf) does not match the classNameFilter class, all animations on that element are disabled. So, when the ngShow animation is processed, no parent animation is detected and the ngShow animation proceeds as usual (which is undesirable in your usecase).

Possible solutions:

  1. Add a class that passes the classNameFilter on the <div ng-if="...">. E.g. class="animate-container".
  2. (This is too specific and thus less robust, but you can) add the ng-hide class on the .box.animate-slide div. This way the class will be already there when ngShow tries to add it and no animation will take place.

@mike-bresnahan
Copy link
Author

Narretz, in case it's not clear, I don't expect the animation to play when the element loads because ng-show is initialized to false thus the element is not initially visible.

gkalpak, solution #1 works like a charm, albeit it is not intuitive to me why that is.

@mike-bresnahan
Copy link
Author

I discovered an alternative solution. Replacing the ng-show with an ng-if with appropriate CSS changes, exhibits the behavior I expect.

https://plnkr.co/edit/IZK0eu2BZ624JdiHsEX8?p=preview

@mike-bresnahan
Copy link
Author

SIDE NOTE: The class name filter was added because a performance increase was observed with Angular 1,3. Today I reexamined the issue with 1.5 and I don't see any performance increase. Therefore I am considering solving this issue by simply removing the class name filter.

@gkalpak
Copy link
Member

gkalpak commented Nov 28, 2016

Since the current behavior seems to be intended and you were able to make it work for you, I am going to close this.

@mike-bresnahan
Copy link
Author

I'm still confused why this is the expected behavior. Is it expected behavior that an element initialized to ng-show=false should be initially visible? And if so, why does ng-if behave differently?

@gkalpak
Copy link
Member

gkalpak commented Nov 29, 2016

The difference is that ngIf is a structural (transclude: 'element') directive. This means that the HTML that it sits on is not treated as a DOM element/subtree, but as a template. It is removed from the DOM, cloned/copied and then it is up to ngIf to decide what to do with it. ngIf will remove or insert such copies from/into the DOM, based on its condition.

ngShow/ngHide on the other hand are "normal" (i.e. non-structural) directives. They simply add/remove a class on their DOM element.

--
This is what is happening (in an over simplified way) while compiling and linking the parent container in your example:

With ngIf on .box

  1. Remove the .box.
  2. Evaluate the ngIf condition (--> false)
  3. Do nothing (the element is already removed).
  4. Later, when the condition evaluates to true, a compiled copy of the initial .box template is linked and inserted into the DOM.

With ngShow on .box

  1. Compile .box without removing it from the DOM. (The element is already there.)
  2. In the post-linking phase, evaluate the ngShow condition.
  3. Since the condition is false, add the ng-hide class to .box. This triggers the animation as one would normally expect.

--
Hopefull this also makes clearer why my 2nd solution from above worked:

  1. Add the ng-hide class on the .box.animate-slide div. This way the class will be already there when ngShow tries to add it and no animation will take place.

The ng-hide class will make the .box disappear initially. When ngShow evaluates its condition for the first time, it doesn't have to add the ng-hide class, because it is already there. No animation happens and the element stays hidden, until the condition evaluates to true.

@mike-bresnahan
Copy link
Author

I think I understand why it behaves the way it does. I'm sorry I didn't make that clear. Referring to ng-if was probably a distraction. My question is, regardless of implementation details, why should we expect an element with ng-show initialized to false to be initially visible?

@gkalpak
Copy link
Member

gkalpak commented Nov 29, 2016

why should we expect an element with ng-show initialized to false to be initially visible?

Maybe we shouldn't. Part of the reason why this happens is 667183a. Because ngShow/ngHide will have the ng-hide class added/removed after one digest, ngAnimate has time to pick up the animation initially.

It might make sense to have the class applied immediatelly (e.g. via element.addClass('ng-hide')) the first time it evaluates the expression. You can see this in practice for ngShow here (which is another workaround for your issue).

That is ngShow/ngHide specific though - not really relate to ngAnimate. I don't know if there are any usecases that might break by such a change. You can open a new issue about that to get the discussion going (and people that might be affected or have an opinion can chime in).

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

No branches or pull requests

3 participants