6

EDIT: So now it's not random and it looks like it always fails to execute from the .css() method (no change were made). Still don't get the mistake I may have made though.


I'm trying to animate the removal of a div with jQuery and animate.css.

Problem is the events and operations this animation depends on literally execute randomly.

This code runs in response to a click, inside an .on("click"... handler:

$('section').on('click', 'button', function() {
  // Remove the selected card
  $(this).closest('.mdl-card')
    .addClass('animated zoomOut')
    .one('animationend', function() {
      empty_space = $('<div id="empty-space"></div>');
      empty_space.css('height', ($(this).outerHeight(true)));
      $(this).replaceWith(empty_space);
    });
  // everything is okay until now
  // setTimeOut() doesn't always execute
  setTimeout(function() {
    console.log("test1");
    // the following doesn't always happen...
    $('#empty-space')
      .css({
        'height': '0',
        'transition': 'height .3s'
          // transitionend doesn't always fire either
      })
      .one('transitionend', function() {
        $('#empty-space').remove();
        console.log("test2");
      });
  }, 300);
  // Upgrade the DOM for MDL
  componentHandler.upgradeDom();
});
/* Animate.css customization  */

.animated {
  animation-duration: .3s
}
<head>
  <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" />
</head>

<body>
  <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>
  <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>
</body>

Depending on the page load, nothing happens, sometimes only the first log, sometimes it only gets to the CSS transition, sometimes it's complete.

Tested on Firefox and Chromium.

I'm probably misinterpretating something, cause it seems really strange.

nem035
  • 34,790
  • 6
  • 87
  • 99
Lucas
  • 93
  • 8
  • With an if statement, which includes other working operations. – Lucas Jan 06 '17 at 15:35
  • Okay, it's in an on('click') event which include other operations that are working well. – Lucas Jan 06 '17 at 15:42
  • 1
    Okay, lots of things to discover when it's not about lurking anymore ! thank you. – Lucas Jan 06 '17 at 15:47
  • One thing that jumps out is that you're attaching an `animationend` handler to multiple elements (`.mdl-card`'s parents). That means you'll get a callback from **each** element that that occurs on (`one` just means you'll only get one from *each* element, not one in total). But then you're using an element with an `id` in the replacement, which suggests you could end up with multiple elements with the same `id` (which is invalid). Subsequently selecting with `$("#empty-space")` will only select the first of those. – T.J. Crowder Jan 06 '17 at 15:48
  • 2
    If you can update the question with a **runnable** [mcve] using Stack Snippets (the `[<>]` toolbar button) demonstrating the problem, it'll be easier for people to help. – T.J. Crowder Jan 06 '17 at 15:49
  • In this case there is sytematically only one .mdl-card in the body, but it indeed would be safer to give it an id. I'll check the doc and come back. – Lucas Jan 06 '17 at 15:56
  • 1
    Sorry, I'd misread the code. It's `$(this).parents(".mdl-card")`, not `$(".mdl-card").parents()`. Sorry about that. (If there's only one, `closest` may be a better option.) – T.J. Crowder Jan 06 '17 at 16:08
  • 1
    @Lucas I've added a working solution with clarification. Writing you a comment here because it was an undeleted answer. – nem035 Jan 06 '17 at 18:44
  • Thank you @nem035, I'm not clear on how to read your solution though ? Sorry, I don't get everything about SO yet. – Lucas Jan 09 '17 at 09:50
  • @Lucas I deleted it because it seemed that people were disagreeing with what I said, I added it back now. – nem035 Jan 09 '17 at 13:50

2 Answers2

2

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>
nem035
  • 34,790
  • 6
  • 87
  • 99
0

As this link (that was provided by someone in the sidebar, I still don't know how that works ?... thank you anyway) points out, setTimeout is not precise enough for this kind of need. It was executing too soon for the contained code to find the tag that was created about 0.3s before.

Lucas
  • 93
  • 8