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.