Find the jQuery Bug #4: Animations Gone Wild
13 Feb 2012Introduction
In this open-ended series Iāll be showcasing a snippet of buggy jQuery code that you might encounter, explain what the problem is, and then identify how you can easily resolve the issue.
You can view other posts in this series...
The Desired Feature
We want to take the following HTML and build a simple jQuery menu that will reveal sub-menus when you hover over each item.
The Buggy Code
The following code snippet is our first attempt at solving the problem, but there is a subtle error. Do you see it?
If you start playing with the menu it appears that it works as intended, but as you continue to use the menu an annoying problem raises its ugly head. If you move your mouse really quick from right to left over the menu youāll see the problem :(
The Underlying Problem
If you played with the example above youāll have noticed that if you interact with the menu really quickly side-to-side then the animations continue over and over and over again, even after youāve moved off the menu completely!
At the root of the problem is an animation queue that has gotten out of hand. jQuery keeps an internal queue to help it know what animation to run next. When you take an element and call one of the animation methods ( .animate()
, .slideDown()
, .slideUp()
, .slideToggle()
, etcā¦ ) what really happens is that effect gets added to the default "fx"
animation queue that is attached to the element. As each effects completes jQuery will move on to the next effect in the queue until all animations are complete.
The magic that we need is to somehow interrupt the queue system. Thankfully, there is an API just for that and in the next section we will show how to use it.
A Solution
The solution to fix this problem is really simple and straightforward. In the words of Bob Newhart, we need the animation to STOP IT!
Of course, we can tell our program to STOP IT!, but they are usually to stubborn to heed to our warnings. Thankfully there is a method we can utilize in jQuery coincidentally called .stop()
which we can use to solve our animation problem!
The following is the documentation from jQueryās website about the .stop()
method that we will use.
.stop( [clearQueue] [, jumpToEnd] )
version added: 1.2clearQueue
- A Boolean indicating whether to remove queued animation as well. Defaults to false.jumpToEnd
- A Boolean indicating whether to complete the current animation immediately. Defaults to false..stop( [queue] [, clearQueue] [, jumpToEnd] )
version added: 1.7queue
- The name of the queue in which to stop animations.clear
- QueueA Boolean indicating whether to remove queued animation as well. Defaults to false.jumpToEnd
- A Boolean indicating whether to complete the current animation immediately. Defaults to false. --http://api.jquery.com/stop/
As you see above there are 2 āoverloadedā methods both with optional parameters. The important parameters that we will use to fix our code snippet are the clearQueue
and jumpToEnd
booleans. In order to get rid of the huge queue of animations on an element and complete whatever animation that was started all we need to do is pass true
as both parametersā¦ $element.stop( true, true )
!
If you test out the code again below youāll notice that it works just as before, but this time the bug we found when moving our mouse quickly back and forth across the menu is now gone!
A Fancy Solution
If we wanted to get fancy, we could choose to update the animation easing algorithm. An easing algorithm is a mathematical equation that defines the animation path of an effect. Wow, that was a mouth full. Letās try that again, but this time with something visual. The following demo, from jQuery UI, is a great visualization of the various easing algorithms. Click the image to launch the jQuery UI Easing Demo and then click on each square to view the easing in action. I think my favorite is āeaseOutBounceā ;)
With these easing options in mind, letās update our example above and use āeaseOutBounceā instead of the default āswingā option that is default in jQuery.
NOTE: "The only easing implementations in the jQuery library are the default, called swing, and one that progresses at a constant pace, called linear." -- http://api.jquery.com/slideDown
You might notice that I also added some other parameters to the .slideDown()
and .slideUp()
methods. All of the parameters are optional, but if we want we can provide our own values such as a duration
, easing
algorithm, and a callback
to invoke when the animation is complete. In the code above, I trigger a custom event depending if the sub-menu was opened
or closed
and then I delegate to those events using the .on()
method on line 19.
.slideDown( [duration] [, easing] [, callback] )
version added: 1.4.3.slideUp( [duration] [, easing] [, callback] )
version added: 1.4.3duration
- A string or number determining how long the animation will run.easing
- A string indicating which easing function to use for the transition.callback
- A function to call once the animation is complete. -- http://api.jquery.com/slideDown/ & http://api.jquery.com/slideUp/
Now you can test the changes we made. The effect may be too adventurous for most production business web applications, but the one I chose is just one of the many easing algorithms provided by jQuery UI that you can pick from. Also, if you are a math wizard you can come up with your algorithm!
Refactoring the Code
If you are anything like me you probably noticed a code smell. Much of the code from the previous examples looks very redundant. Letās take a stab at refactoring the code somewhat to reduce the āduplicated codeā smell and made the code DRY (donāt repeat yourself).
If you look through your vast code base, And notice code repeated here and there. You might consider refactoring soon, Or you'll experience maintenance despair!
Previously we were passing two functions to the .hover()
method. The 1st function parameter was to handle mouseover
events and the other was to handle mouseout
events. Thankfully, there is another āoverloadedā version of .hover()
that takes just 1 function parameter. This 1 function will be invoked on both mouseover
events and mouseout
events!
You may also notice that we are using a different method to actually perform the sub-menu animation. jQuery includes a .slideToggle()
method that will either .slideDown()
or .slideUp()
depending on the state of the element.
The last thing to take into consideration is how to know which custom event to trigger. Previously I knew which event to trigger because opened
was associated with .slideDown()
and closed
was associated with .slideUp()
, but what about now? Thankfully that is easily solved by looking at the event
object passed to the hover
event handler. The event
object has a type
property which tells what type of event was originally fired. So, I can do something like thisā¦ e.type === "mouseover" ? "opened" : "closed"
ā http://jsfiddle.net/ujsfH/
Conclusion
The key concept to remember here is that jQuery has an internal animation queue that you should be aware of. If you need to clear out that queue you can use the .stop()
method. Also, you can modify the duration of animation, change the animation easing algorithm, and also respond to an event when the animation is complete.
Things we didnāt cover in this post are how to create your own queue, how to create your own easing algorithm, how to modify the default animation duration, and several other concerns.
Until next timeā¦
NOTE: The CSS I used as the base for the example menu above is a modified version from a blog post on @Kriesi's website. I removed the hover styles and replaced his jQuery code with mine for this Find the jQuery Bug post.