Angular.js: ng-show/hide is delayed on an element that is animating

Created on 16 Jul 2014  ·  17Comments  ·  Source: angular/angular.js

This is not expected behavior if you're doing a constant animation on something like a loading spinner and just want to hide or show it immediately.

The element was not told to be managed by ng-animate, I just want normal ng-show/hide to happen instantly, and any css animations that happen to be ongoing should be irrelevant, I think.

Fiddle: http://jsfiddle.net/PvS8k/2/

Uncheck "show loader" and note that it takes a few seconds to actually go away.

I think this is a recent change because our loader wasn't sticking in our application before upgrading. I think we were on 1.12 before.

I must be missing something in my understanding of how to use/not use ng-animate, right? In this case I actually want to NOT use it, but if I can use it to ignore this element, that would work too. Definitely surprising behavior though.

ngAnimate broken expected use bug

Most helpful comment

@tole42 This is the workaround:

.YourElementClass.ng-animate { -webkit-animation: none 0s; }

All 17 comments

ngAnimate is greedy since it assumes that all keyframes/transition code that is active on the element during any of the triggers are there to be animated via ngAnimate. Therefore, in this case, despite .loader having nothing to do with ngShow / ngHide, it is still picked up as a valid animation.

To get around this, simple erase the animation styling when the .ng-animate class is applied on the element.

http://jsfiddle.net/PvS8k/4/

This is a problem with CSS inheritance. If there was a better way to detect styles without having to solely rely on getComputedStyle then these kinds of quirks could possibly be avoided.

@matsko I think this special case is actually a regression. As you can see in @SimpleAsCouldBe 's plunker, or in this plunker http://plnkr.co/edit/5cfIXfNryNOzK66q9YWL?p=preview, in 1.2.17, the element does not disappear immediately, but in 1.2.16, it does. This is most likely because of https://github.com/angular/angular.js/commit/55b2f0e8620465559016b424967d90a86af597c0
I think this warrant a little closer inspection; at least there should be a note in the changelog and docs about this.

Thanks for the workaround, it does the trick as expected.

Two points. One is specific to ng-show and one is general.

ng-show + infinite animations:

Should an infinite animation _really_ delay an ng-show's hide? I tried counting seconds and it's not always 4 seconds that it waits to hide it. Sometimes its 5 or 7... seems to vary. Also If you flip the checkbox on and off again it never hides. I think there's an actual bug hiding in here somewhere.

Philisophical

As a non-contributor I don't have much say, but maybe opt-out isn't the right strategy for this library. It's just so astoundingly magical to have a JS library react to CSS rules.. If I had to tag elements with ng-animatable I would be much more prepared for this.

@SimpleAsCouldBe yes I've been thinking the same that maybe it might be best to require all animations to have a animate- class prefix such that ngAnimate would then hook into the animations.

Regarding the delay, it's not that there is a delay, it's that ngAnimate cancels out the infinite portion and only runs the animation once. Therefore, when you hide it, it waits for 2s * 1.5 = 3 seconds and then it runs the timeout check and closes of the animation. So it's an unnecessary window of time that shouldn't be there at all--only CSS style detection is highly limited.

@Narretz while the commit you provided may effect this particular example, it doesn't actually have anything to do with the regression. The reason for this is because the .ng-hide styling that was there before the fix applied the display:none value at a different time. But the current fix applies it ONLY after all the animations are done (once the .ng-animate class is removed).

Have a look at the demo below. See how there are two sets of selectors for the .blue class: one with .ng-animate and without it. Despite the .blue class being applied immediately, ngAnimate still thinks an animation is due and will wait for 3s (remember 2s * 1.5) before applying a closing timeout. Normally it would be 2s, but a animationend event is never fired since the actual animation on the element (which is there because of .loader) is infinite and is not-triggered by ng-class (or ng-show for the previous example). So that being said this doesn't get effected due to the prior bug fix.
http://jsfiddle.net/PvS8k/15/

It might be best to make ngAnimate less naive and only animate when animated- is there as a CSS prefix somewhere.

I think if you're open to a breaking change like opt-in animation handling, it would create a less surprising developer experience.

Would you be open to prefixing with something angular specific like .ng-animate rather than plain-old .animate?

I have the same Problem:
With angular >=1.2.17 http://jsfiddle.net/9krLr/17/
With angular 1.2.16 everthing is fine: http://jsfiddle.net/EZpQQ/1/

@tole42 This is the workaround:

.YourElementClass.ng-animate { -webkit-animation: none 0s; }

Thanks for your help. The workaround is not working :(
What is wrong?
http://jsfiddle.net/9krLr/21/

@tole42 Nothing in that fiddle animates, as far as I can see. Is the animation part of the Foundation library? A more direct re-creation case would make this easier to debug. Doesn't look like the same issue to me.

I believe I see the same issue as @tole42 . With ngAnimate include in a module, the standard ng-hide/ng-show functionality doesn't work correctly for the base case when you are _not_ trying to animate them. In other words, when you use ng-hide/ng-show on a plain element with no animation class applied.

<div ng-hide="hideme">hide me</div>

If I wanted an animation on that hide, I would do something like:

<div ng-hide="hideme" class="myanimation">hide me</div>

There are some instances in an app where you may want to animate a show/hide and other instances where you don't want to animate. In the instances where you don't want to animate, you don't decorate the element being shown/hidden with any custom animation class. In this instance, Angular just adds the "ng-hide" class to a hidden element.

If you don't include ngAnimate in your module at all, this works as expected: the element disappears immediately. But by simply including ngAnimate, you get the behavior shown in the two fiddles that @tole42 posted: there is a delay before the element that got the "ng-hide" class added actually disappears. It should disappear immediately if you are not trying to use animations on it.

A couple of work-arounds that seem to work are:

.ng-hide-add {
    transition: 0s linear all;
}

Or:

.ng-hide {
    display: none !important;
}

But it doesn't seem correct that those should be necessary in the case where you are simply trying to hide something in a non-animated manner.

Just to follow up on some additional discoveries...

Trying to boil down the problem to the simplest possible scenario - showing/hiding plain divs - the problem does not manifest.

I noticed in the fiddles posted by @tole42 that the foundation.css was being used, and the example does show the problem with showing/hiding input fields. My experience was similar (seeing the problem manifest with input fields), but using bootstrap.css. Digging into both of those css files more closely, they both apply css transitions to input fields.

Those css transitions appear to be the real culprit, as angular-animate is picking up the durations from them. That is what causes the visual delay when showing/hiding those elements, even if you are not intentionally trying to animate that transition.

So I don't think this is necessarily a problem that Angular should attempt to solve. It's more of a side-effect of using angular-animate with other css libraries, which may have some embedded css transitions that you are not expecting!

Took a long time to track down the root cause, but it was good to finally find a reason that makes sense.

Yes this is an important issue with ngAnimate, but due to the lack of control of getComputedStyle and the cascade of transitions in other libraries, ngAnimate is unable to detect this.

There are two solutions to fixing this:

1) Override the styling for the .ng-animate class:
http://jsfiddle.net/9krLr/27/

The reason why it didn't work for you before was because of the lack of CSS specificity.

2) Use $animateProvider.classNameFilter(regex). The regex in this case will place a hard block on all ngAnimate triggered animations and only allow the animation to take place if the regex matches a className that is present on the element that you want to be animated.

Oh, putting the config in a provider is a nice way to go, I like that a lot, @matsko!

@pnutshellmenace thanks for the quick solution. It saved my day.

I don't understand what the final solution here was. @matsko stated:

It might be best to make ngAnimate less naive and only animate when animated- is there as a CSS prefix somewhere

But I don't see any implementation like that. I took a look at $animateProvider.classNameFilter(regex) but again, I am left with more questions than answers. What is a practical way to implement this and what if there is more than one class name that is applicable?

I see no reason why ngAnimate should assume I want to animate everything. There will always be things I don't want to animate, and currently I have more that I do not want to animate than the other way around, which makes manually styling them to disable transitions unappealing.

Edit: Found this blog entry that puts things into perspective... I'm going to give it a try: http://blog.fgribreau.com/2014/03/how-to-configure-ng-animate-to-work-on.html

Edit 2: Adding $animateProvider.classNameFilter(/enable-animate/); worked, but in the process I found some interesting behaviour:

I had included a .js file of a third-party angular module that had a dependency on ngAnimate, and somehow that was causing animations to be triggered even on my own module. Somehow ngAnimate was affecting the parent module scope?

I would prefer this answer using $scope.$evalAsync(); for async operation. works well.

Was this page helpful?
0 / 5 - 0 ratings