1

I'm trying to place an element arbitrarily on the page, and then animate it through several sequential transitions. The element starts at 0,0 (top left) and then I move it to its starting place (e.g. 100, 100) via an async/await method; then I follow with the transitions. However, unless I use a setTimeout() in the initial placement function, the subsequent transitions occur immediately based on the elements 0,0 position rather than it's arbitrary starting position (100, 100).

There are a number of work arounds. But, I want to know why is the await not working? Note that the await does work if I include a setTimeout.

addTransition = (el, styleProps, durationMs = 0) => {
  return new Promise((resolve, reject) => {
    function handleTransitionEnd() { resolve(el);}
    el.addEventListener("transitionend", handleTransitionEnd, { once: true });
    if (durationMs > 0) el.style.setProperty("transition", `${durationMs}ms`);
    Object.entries(styleProps).forEach(([prop, value]) => { el.style.setProperty(prop, value); });
  });
}

setCSSProperty = (el, prop, val) => {
  return new Promise((resolve, reject) => {
    document.styleSheets[0].rules[0].style.removeProperty("transition");
    el.style.setProperty(prop, val);

//If I remove the following setTimeout, or set the wait to 0, then in the animate function the code continues to run without waiting for this promise to resolve.
    setTimeout(() => {
      resolve(el);
    }, 1);  //********** in codepen I can make this as small as 1. On my i86 desktop I have to make this at least 15
  });
}

async function animate() {
  const actor = document.getElementById("actor");
  let x = ((window.innerWidth - actor.getBoundingClientRect().width) * .5);
  await this.setCSSProperty(actor, "left", x + "px");
  await this.addTransition(actor, { left: "40px", }, 2000);
}

animate()
.actor {
  border-radius: 25px;
  width: 50px;
  height: 50px;
  background: #f80;
  position: absolute;
  top: 0;
  left: 0;
  transition: 3000ms ease-out;
}
<div class="actor" id="actor"></div>

CodePen

Adam Cole
  • 187
  • 1
  • 5
  • 3
    You need to give the browser time to update and repaint the UI before resolving the promise. `setTimeout` is what's known as a **macrotask**, so it runs *after* the event loop has completed and the UI repainted. See the [event loop](https://javascript.info/event-loop). If you omit the `setTimeout`, you are resolving the promise before the browser repaints the UI. – kelsny Feb 27 '23 at 20:16
  • 1
    Instead of `setTimeout()` you may use [`.requestAnimationFrame()`](https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame) for such display related async tasks. – Redu Feb 27 '23 at 20:23
  • 1
    requestAnimationFrame will fire before the next paint, setTimeout is a gamble. You can [force a synchronous reflow](https://stackoverflow.com/questions/55134528/css-transition-doesnt-start-callback-isnt-called/55137322#55137322), but here, just use the [Web Animation API](https://developer.mozilla.org/en-US/docs/Web/API/Element/animate). – Kaiido Feb 27 '23 at 21:12
  • @Kaiido thank you! The reflow was a breeze and did the trick. I suppose it is a macrotask in the event loop, which happens to re/calculate all potentially unresolved CSS positions. – Adam Cole Feb 27 '23 at 23:24
  • 1
    It's synchronous. The thing is that in best circumstances, the browser can wait until right before the page is painted to recalculate all the boxes (actually it's right before the ResizeObserver's callbacks are fired, which is right after rAF callbacks fired). They do so because recalculating all the boxes is a heavy operation and it's best to get it done only once with all the up to date info any JS callback could have made. But some getters need the boxes to be up to date, so they'll trigger the reflow synchronously. But once again, for your case you'd be better switching to Web Animations. – Kaiido Feb 28 '23 at 00:13

0 Answers0