Even though you give setTimeout
and animation the same duration value, the order of execution for their callbacks isn't actually guaranteed. The simplified reason is the following:
JS is essentially single-threaded meaning it can execute 1 thing at a time. It has an event loop that has a bunch of event queues that all accept callbacks for things like network requests, dom events, animation events etc and out of all of these, only one runs at a time and runs until the end (Run To Completion Semantic). Further complications due to this single-threadiness are that things like repaints and garbage collection might also run on this thread so additional unpredictable delays might happen.
Useful resources:
This means that, although you're delaying the height transition of the empty element to after the zooming out of the parent element, the duration of this delay isn't consistently guaranteed due to the above-stated factors. Thus the empty element might not be present when the callback within setTimeout
gets called.
If you increase the delay for setTimeout
to a larger value , the height animation of the empty element will actually happen more often than not, since this will increase the gap between the zoomOut
animation ending and code within setTimeout
starting, meaning the empty element will most likely be in the DOM before we start transitioning its height.
However, there isn't really a guaranteed way of determining the minimum value for this delay because each time it could be different.
What you must do is code in such a way that the execution order of the animationend
and setTimeout
callbacks doesn't matter.
Solution
First things first, you don't need the extra empty space, you can perform both the zoomOut
animation and height transition on the same element.
One thing you must be aware is that the css library you're using already sets min-height
of .mdl-card
to a certain value (200px
), so you must transition on this property because the height of the element could be less than this value. You also want to transition on the height
itself so you can remove the element without any jank. Finally, you must delay the removal of the element after both animation and transition are finished.
Here's a working solution:
$('section').on('click', 'button', function() {
var isAnimationDone = false,
isTransitionDone = false;
var $item = $(this).closest('.mdl-card');
$item
.addClass('animated zoomOut')
.one('animationend', function() {
isAnimationDone = true;
onAllDone();
});
$item
.css({
height: 0,
'min-height': 0
})
.one('transitionend', function() {
isTransitionDone = true;
onAllDone();
});
function onAllDone() {
if (isAnimationDone && isTransitionDone) {
$item.remove();
}
}
});
.animated {
animation-duration: 300ms
}
.mdl-card {
transition: min-height 300ms, height 300ms;
}
<link href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/3.5.2/animate.min.css" rel="stylesheet" />
<link href="https://code.getmdl.io/1.3.0/material.indigo-pink.min.css" rel="stylesheet" />
<script src="https://code.getmdl.io/1.3.0/material.min.js"></script>
<script src="https://code.jquery.com/jquery-3.1.1.min.js"></script>
<section>
<div class="mdl-card">
<button class="mdl-button mdl-js-button">Close</button>
</div>
<p>
Content to test the height of the div above
</p>
</section>
With Promises this becomes a bit easier:
function animate($item, animClass) {
return new Promise((resolve) => {
$item.addClass(`animated ${animClass}`).one('animationend', resolve);
});
}
function transition($item, props) {
return new Promise((resolve) => {
$item.css(props).one('transitionend', resolve);
});
}
$('section').on('click', 'button', function() {
const $item = $(this).closest('.mdl-card');
// start animation and transition simultaneously
const zoomInAnimation = animate($item, 'zoomOut');
const heightTransition = transition($item, {
height: 0,
'min-height': 0
});
// remove element once both animation and transition are finished
Promise.all([
zoomInAnimation,
heightTransition
]).then(() => $item.remove());
});
.animated {
animation-duration: 300ms
}
.mdl-card {
transition: min-height 300ms, height 300ms;
}
<link href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/3.5.2/animate.min.css" rel="stylesheet" />
<link href="https://code.getmdl.io/1.3.0/material.indigo-pink.min.css" rel="stylesheet" />
<script src="https://code.getmdl.io/1.3.0/material.min.js"></script>
<script src="https://code.jquery.com/jquery-3.1.1.min.js"></script>
<section>
<div class="mdl-card">
<button class="mdl-button mdl-js-button">Close</button>
</div>
<p>
Content to test the height of the div above
</p>
</section>