As of the time of writing, the original answer is now 8 years old. Still I feel like there isn't yet a proper solution to the original question.
Bootstrap has gone a long way since then and is now at 4.5.2. This answer addresses this very version.
The issue with all the other answers is, that while they hook into show.bs.dropdown
/ hide.bs.dropdown
, the follow-up events shown.bs.dropdown
/ hidden.bs.dropdown
are either fired too early (animation still ongoing) or they don't fire at all because they were suppressed (e.preventDefault()
).
Since the implementation of show()
and hide()
in Bootstraps Dropdown
class share some similarities, I've grouped them together in toggleDropdownWithAnimation()
when mimicing the original behaviour and added little QoL helper functions to showDropdownWithAnimation()
and hideDropdownWithAnimation()
.
toggleDropdownWithAnimation()
creates a shown.bs.dropdown
/ hidden.bs.dropdown
event the same way Bootstrap does it. This event is then fired after the animation completed - just like you would expect.
/**
* Toggle visibility of a dropdown with slideDown / slideUp animation.
* @param {JQuery} $containerElement The outer dropdown container. This is the element with the .dropdown class.
* @param {boolean} show Show (true) or hide (false) the dropdown menu.
* @param {number} duration Duration of the animation in milliseconds
*/
function toggleDropdownWithAnimation($containerElement, show, duration = 300): void {
// get the element that triggered the initial event
const $toggleElement = $containerElement.find('.dropdown-toggle');
// get the associated menu
const $dropdownMenu = $containerElement.find('.dropdown-menu');
// build jquery event for when the element has been completely shown
const eventArgs = {relatedTarget: $toggleElement};
const eventType = show ? 'shown' : 'hidden';
const eventName = `${eventType}.bs.dropdown`;
const jQueryEvent = $.Event(eventName, eventArgs);
if (show) {
// mimic bootstraps element manipulation
$containerElement.addClass('show');
$dropdownMenu.addClass('show');
$toggleElement.attr('aria-expanded', 'true');
// put focus on initial trigger element
$toggleElement.trigger('focus');
// start intended animation
$dropdownMenu
.stop() // stop any ongoing animation
.hide() // hide element to fix initial state of element for slide down animation
.slideDown(duration, () => {
// fire 'shown' event
$($toggleElement).trigger(jQueryEvent);
});
}
else {
// mimic bootstraps element manipulation
$containerElement.removeClass('show');
$dropdownMenu.removeClass('show');
$toggleElement.attr('aria-expanded', 'false');
// start intended animation
$dropdownMenu
.stop() // stop any ongoing animation
.show() // show element to fix initial state of element for slide up animation
.slideUp(duration, () => {
// fire 'hidden' event
$($toggleElement).trigger(jQueryEvent);
});
}
}
/**
* Show a dropdown with slideDown animation.
* @param {JQuery} $containerElement The outer dropdown container. This is the element with the .dropdown class.
* @param {number} duration Duration of the animation in milliseconds
*/
function showDropdownWithAnimation($containerElement, duration = 300) {
toggleDropdownWithAnimation($containerElement, true, duration);
}
/**
* Hide a dropdown with a slideUp animation.
* @param {JQuery} $containerElement The outer dropdown container. This is the element with the .dropdown class.
* @param {number} duration Duration of the animation in milliseconds
*/
function hideDropdownWithAnimation($containerElement, duration = 300) {
toggleDropdownWithAnimation($containerElement, false, duration);
}
Now that we have written proper callbacks for showing / hiding a dropdown with an animation, let's actually bind these to the correct events.
A common mistake I've seen a lot in other answers is binding event listeners to elements directly. While this works fine for DOM elements present at the time the event listener is registered, it does not bind to elements added later on.
That's why you are generally better off binding directly to the document
:
$(function () {
/* Hook into the show event of a bootstrap dropdown */
$(document).on('show.bs.dropdown', '.dropdown', function (e) {
// prevent bootstrap from executing their event listener
e.preventDefault();
showDropdownWithAnimation($(this));
});
/* Hook into the hide event of a bootstrap dropdown */
$(document).on('hide.bs.dropdown', '.dropdown', function (e) {
// prevent bootstrap from executing their event listener
e.preventDefault();
hideDropdownWithAnimation($(this));
});
});