14

What I'm doing and what's wrong

When I click on a button, a slider shows up. (here is an example of what it looks like, do not pay attention to this code)

The slider shows via an animation. When the animation is finished I should include an HTML page I've loaded from the server. I need to apply the HTML in the slider after the animation otherwise the animation stops (the DOM is recalculated).

My algorithm

  1. Start the request to get the HTML to display inside the slider
  2. Start the animation
  3. Wait the data to be ready and the transition to be finished

    Why? If I apply the HTML during the animation, it stops the animation while the new HTML is added to the DOM. So I wait for both to end before step 4.

  4. Apply the HTML inside the slider

Here is the shortened code:

// Start loading data & animate transition
var count = 0;
var data = null;

++count;
$.get(url, function (res) {
    data = res;
    cbSlider();
});

// Animation starts here

++count;
$(document).on('transitionend', '#' + sliderId, function () {
    $(document).off('transitionend', '#' + sliderId);
    cbSlider()
});

function cbSlider() {
    --count;
    // This condition is only correct when both GET request and animation are finished
    if (count == 0) {
        // Attempt to enforce the frame to finish (doesn't work)
        window.requestAnimationFrame(() => { return });

        $('#' + sliderId + ' .slider-content').html(data);
    }
}

The detailed issue

transitionend is called too early. It makes the last animated frame a lot too long (477.2ms) and the last frame is not rendered at transitionend event.

Google Chrome Timeline

From the Google documentation, I can tell you that the Paint and Composite step of the Pixel Pipeline is called after the Event(transitionend):

Pixel Pipe Line

Maybe I'm overthinking this.

How should I handle this kind of animations?

How can I wait the animation to be fully finished and rendered?

Elfayer
  • 4,411
  • 9
  • 46
  • 76
  • Can you explain how `count` is being used - normally counters are for loops, not clear what they are doing for you here. – serraosays Aug 26 '16 at 13:08
  • The variable `count` is used to ensure the callback `cbSlider` is only executed once, after both the GET request **and** the animation has ended (via the event `transisionend`). I increment `count` once before the request and once before the event is listening. Those two parts are calling the same callback `cbSlider`. In the `cbSlider` function I decrement the `count` variable once at every call. It is called twice, the second time it will be called the condition will be correct and it will execute. Does that make sense ? – Elfayer Aug 26 '16 at 13:19
  • I'm not necessarily saying what you are doing is wrong but I wouldn't use a counter for this. I'd probably use `.ajax()` instead of `.get()`, and make the `success` parameter of `.ajax()` the animation, so it only happens once the request is successful. I could code up what I'm think if you want to see it. – serraosays Aug 26 '16 at 13:52
  • @staypuftman That's not a good idea. If the request takes 5s, you won't have any visual feedback during 5s... – Elfayer Aug 26 '16 at 13:57
  • Then perhaps that way to long loading time is rather what you should be working on. – CBroe Aug 26 '16 at 14:06
  • @CBroe That's just an example. I'm trying to optimize at most. I want the request to start before the animation, this way the user waits 300ms during the animation, and he doesn't know I'm already loading his data. – Elfayer Aug 26 '16 at 14:09
  • You could have some loader going while the ajax request is being completed - I do this all the time. You could also have a faux JSON result sitting in an array variable - if the request times out, I just load the array variable. That way you can control how long you want to wait. – serraosays Aug 26 '16 at 14:17
  • Why does the HTML have to be loaded via AJAX? If it is always to be shown after the animation, then why isn’t it part of the same page to begin with? Is it different HTML each time? – CBroe Aug 26 '16 at 14:20
  • 2
    I think the op is not complaining about time, but only asking how to add his data after the end of the frame rendering. Not when `transitionend` event is raised, but when the render of the frame is done and printed to screen. – Guillaume Beauvois Aug 26 '16 at 14:21
  • @CBroe It's a big HTML (as big as a page with tabs navigation, etc.). I don't want to load this HTML with the page behind, I'd be loading two pages on first load, also this page is compiled on the server and its content depends on the user selection. – Elfayer Aug 26 '16 at 14:24
  • @staypuftman My "loader" is in the slider. It is a rough example that I gave you, there is actually a loader icon inside while the transition is happening and the HTML is not included in the slider. I don't understand your "faux JSON" solution – Elfayer Aug 26 '16 at 14:52
  • Just wanted to mention that technically, the `cbSlider` callback could be executed twice, if the AJAX request would complete before the second `++count` is executed. That is mostly a theoretical issue though (could be fixed by initializing `count` to 2). Does it help to put the page update in `setTimeout(cbSlider, 0)` instead of calling it directly? Note that even though the delay is 0, this will force the function to be executed asynchronously, which might (should?) queue if after the actual paint? – Just a student Aug 29 '16 at 09:20
  • You are not using `mozTransitionEnd` and `webkitTransitionEnd`, and `webkitRequestAnimationFrame()` and `mozRequestAnimationFrame()`. Maybe it's a problem of compatibility? I use very often this functionality and it works fine. – Marcos Pérez Gude Aug 29 '16 at 09:34
  • Here you are a little help: http://stackoverflow.com/questions/2794148/css3-transition-events || http://stackoverflow.com/questions/5023514/how-do-i-normalize-css3-transition-functions-across-browsers – Marcos Pérez Gude Aug 29 '16 at 09:40
  • I think that `transitionend` is supported [pretty well](http://caniuse.com/#search=transitionend) by now. – Just a student Aug 29 '16 at 10:37
  • @Justastudent in caniuse, you can see in Notes section the following: `The prefixed name in WebKit browsers is webkitTransitionEnd` – Marcos Pérez Gude Aug 29 '16 at 10:54
  • @Elfayer just for test, try with `webkitTransitionEnd` and `webkitRequestAnimationFrame()`. – Marcos Pérez Gude Aug 29 '16 at 10:57
  • @Justastudent You're right, that's safer. ;) `setTimeout` doesn't change anything. – Elfayer Aug 29 '16 at 13:16
  • @MarcosPérezGude It doesn't seem to change anything. The event is called, so I guess that works as expected. – Elfayer Aug 29 '16 at 13:16
  • Can you include `css`, `javascript` at Question where `transitionend` is used? – guest271314 Aug 30 '16 at 00:21
  • There are multiple elements which have `transition` set at `css` – guest271314 Aug 30 '16 at 00:27
  • @guest271314 There are 4 transitions happening. The first is the loading icon on the slider (see: http://fontawesome.io/examples/#animated), the other three, you can see them on the demo I provided: http://jsfiddle.net/ As you can see there are 3 transitions on the CSS. – Elfayer Aug 30 '16 at 07:12
  • @Elfayer `javascript` at Question is different from `javascript` at http://jsfiddle.net/qv9jtn5g/ ? – guest271314 Aug 31 '16 at 01:32
  • I know I'm late to the party but are you animating multiple properties? If so you could be getting your callback on an animation finishing before the animation you want. You can see which property is being animated in the `transitionend` callback. I've had success with fixing this issue by specifying my `transitionend` callback to only run for a specific property. – Jasper Sep 01 '16 at 15:54

2 Answers2

2

I'm not sure why transitionend is fired before the last frame has rendered, but in this (very crude) test it seems that a setTimeout does help...

The first example shows how the html calculation and injection happens too early. The second example wraps the long running method in a setTimeout and doesn't seem to trigger any interuption in the animation.

Example 1: reproduction of your problem

var ended = 0;
var cb = function() {
  ended += 1;

  if (ended == 2) {
    $(".animated").html(createLongHTMLString());
  }
}

$(".load").click(function() {
  $(".animated").addClass("loading");
  $(".animated").on("transitionend", cb);
  setTimeout(cb, 100);
});

function createLongHTMLString() {
  var str = "";
  for (var i = 0; i < 100000; i += 1) {
    str += "<em>Test </em>";
  }
  return str;
};
.animated,
.target {
  width: 100px;
  height: 100px;
  position: absolute;
  text-align: center;
  line-height: 100px;
  overflow: hidden;
}
.target,
.animated.loading {
  transform: translateX(300%);
}
.animated {
  background: green;
  z-index: 1;
  transition: transform .2s linear;
}
.target {
  background: red;
  z-index: 0;
}
.wrapper {
  height: 100px;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>

<div class="wrapper">
  <div class="animated">Loading</div>
  <div class="target"></div>
</div>

<button class="load">load</button>

Example 2: in which a setTimeout seems to fix it

With a setTimeout around the html injection code.

var ended = 0;
var cb = function() {
  ended += 1;

  if (ended == 2) {
    setTimeout(function() {
      $(".animated").html(createLongHTMLString());
    });
  }
}

$(".load").click(function() {
  $(".animated").addClass("loading");
  $(".animated").on("transitionend", cb);
  setTimeout(cb, 100);
});

function createLongHTMLString() {
  var str = "";
  for (var i = 0; i < 100000; i += 1) {
    str += "<em>Test </em>";
  }
  return str;
};
.animated,
.target {
  width: 100px;
  height: 100px;
  position: absolute;
  text-align: center;
  line-height: 100px;
  overflow: hidden;
}
.target,
.animated.loading {
  transform: translateX(300%);
}
.animated {
  background: green;
  z-index: 1;
  transition: transform .2s linear;
}
.target {
  background: red;
  z-index: 0;
}
.wrapper {
  height: 100px;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>

<div class="wrapper">
  <div class="animated">Loading</div>
  <div class="target"></div>
</div>

<button class="load">load</button>
user3297291
  • 22,592
  • 4
  • 29
  • 45
  • Actually it doesn't work in my case. I have to put a `setTimeout` of 10 or 100 ms to make it work. It doesn't wait the frame to render. – Elfayer Sep 05 '16 at 08:32
  • 3
    Have you made sure the `transitionend` event is coming from the right property? You can check the property through `event.originalEvent.propertyName`. In the example above, you'd want to make sure it's the `transform` property. – user3297291 Sep 05 '16 at 08:38
  • 1
    @user3297291 that's a great point. This helped me discover a very important problem in my code. And I managed to skip an un-needed hardcoded timeout in my code. – Andrei Glingeanu Aug 16 '23 at 10:46
0

Well, if transitions are not working for you the way you want to, you can go back a few years and use jQuery animations instead?

(function(slider){
    $.get(url, function (res) {
        slider.animate({
            // put whatever animations you need here
            left: "5%",
        }, 5000, function() {
            // Animation complete.
            slider.find('.slider-content').html(res);
        });
    });
}($('#' + sliderId)));

You can also start both actions at the same time, and then add the html to the document only after the animation has finished and the request is complete, but that would require a flag.

(function(slider){

    // whether the animation is finished
    var finished = false;

    // whether the html has been added already
    var added = false;

    // your html data
    var html = null;

    function add() {
        if (finished && html && !added) {

            // make sure function will only add html once
            added = true;

            slider.find('.slider-content').html(html);
        }
    }

    $.get(url, function (res) {
        html = res;
        add();
    });

    slider.animate({
        // put whatever animations you need here
        left: "5%",
    }, 5000, function() {
        // Animation complete.
        finished = true;
        add();
    });

}($('#' + sliderId)));
php_nub_qq
  • 15,199
  • 21
  • 74
  • 144