0

I have a dynamically created progress bar (Bootstrap) that is created ASAP on page load, inserted into the DOM and then it immediately starts progressing (the bar starts to fill up). This progress bar has a transition animation: when the bar's width increases (via JavaScript) it shall do so smoothly.

The progress comes in discrete steps: say 4 resources are retrieved asynchronously, when each one is loaded the progress bar increases 25 %.

The problem: The loading of resources represented by the progress bar is sometimes very quick (I suspect there may be some caching involved). This results, sometimes, in the bar instantly changing width, completely skipping the transition animation and the subsequent ontransitionend event (because there was no transition). For a nice visual effect my code awaits the ontransitionend event, which means the code is stuck when this happens.

After painstaking debugging I narrowed it down to the following cause: The creation, insertion and the almost immediate full progression of the progress bar is so fast the bar hasn't even been rendered yet before it's over. As such, when the rendering do occur the bar is already "full" which means the animation is skipped, as the bar is rendered full directly.

Some pseudo code-ish examples (as the code is large and spread out).

main.js:

// Global variable
progressBar = createProgressBar();

await new Promise(function(resolve) {
    // Await progress bar to fill up
    progressBar.oncomplete(resolve);

    // Let's say 4 resources
    loadResources(function() {
        // Callback function, invoked for each resource when it has loaded
        // Additive 25 % increases for a total of 100 %
        progressBar.progress(25);
    });

});

// Finally, hide progressBar, show content

progress.js:

function createProgressBar() {
    var completeCallback = function() {};
    var percentage = 0;
    var progressBar = ...; // Create element for progress bar
    var parent = document.getElementById("progressBarContainer");

    // Insert progress bar into DOM
    parent.appendChild(progressBar);

    // Register element transition event handler
    progressBar.ontransitionend = function() {
        if(percentage === 100)
            completeCallback();
    };

    function oncomplete(callback) {
        completeCallback = callback;
    }

    function progress(inc) {
        percentage += inc;
        progressBar.style.width = ( percentage + "%" );
    }

    return { 
        oncomplete: oncomplete,
        progress:   progress
    };
}

This is essentially what happens, the progress() will be called 4 times before render and as such the bar starts full and no transitionend event is called.

I then used some google magic and found this article that showcased an example for how to wait for animation frames. I added the following code into the progress.js createProgressBar() body:

var loopCount = 0;

var observer = new MutationObserver(async function(mutations) {
    // Await for two animation frames
    await new Promise(function(resolve) {
        function loop(){
            window.requestAnimationFrame(function() {
                loopCount++;
                if(loopCount === 2) {
                    resolve();
                } else {
                    loop();
                }
            });
        }

        // Start loop
        loop();
    });

    // Trial and error, this code will now yield transition animation
    progressBar.style.width = "100%";
    observer.disconnect();
});

observer.observe(components.wrapper, { attributes: false, childList: true, characterData: false, subtree: true });

I tried awaiting one frame but I still had the same problem. In the example I await two frames, which seems to have done the trick as I can no longer reproduce the error. (That last piece kind of breaks the code before it as I set the width directly to produce the error.)

The question(s):

Am I right here, is the effect due to the rendering coming too late?

Secondly, is there another cleaner way to address this other than using MutationObserver and requestAnimationFrame or a timeout? I haven't been able to find an event for this unfortunately. While this solution works, i don't like the feeling of "picking" a number that works in case the error comes back.

halfer
  • 19,824
  • 17
  • 99
  • 186
Empha
  • 156
  • 1
  • 1
  • 8
  • 1
    Set an initial width, either in CSS or in your constructor (`progressBar.style.width = 0`), then after `parent.appendChild(progressBar);` force a reflow, in the next animation frame (right before the next paint) is a good place for it -> `parent.appendChild(progressBar); requestAnimationFrame( () => parent.offsetWidth );` this way you are sure the next change to it's `style.width` will trigger the transition. – Kaiido Apr 25 '20 at 13:35
  • 1
    Oh boy, the proposed answered question is like it was written by myself, i previously tried what both its answers suggests but i brushed the effects of as a kind of coincidence... Thanks alot! – Empha Apr 25 '20 at 13:46

1 Answers1

0

Why don't you make pure HTML and CSS progress bar and control its state using js - changing class.

Vivek
  • 86
  • 4