3

I'm trying to create an action where a div is created and immediately "floats" upwards until it's off-screen.

To accomplish this I'm attempting to use a CSS transition, which will be completely driven by JavaScript (due to limitations in my use-case).

A problem occurs when I create an element, assign it it's transition style properties, and then immediately try to kick-off the transition by making a style change (top).

It looks like a timing issue is happening where the top style change is firing before the transition becomes available and thus simply moving my div off-screen immediately, rather than actually performing the transition.

Here's a simplified example:

var
    defaultHeight = 50,
    iCount = 0,
    aColors = ['red', 'orange', 'yellow', 'green', 'blue', 'purple'];

function createBlock() {
    var testdiv = document.createElement('div');
    testdiv.className = 'testdiv';
    document.body.appendChild(testdiv);
    testdiv.style.left = '50%';
    testdiv.style.backgroundColor = aColors[iCount % aColors.length];
    testdiv.style.width = defaultHeight + 'px';
    testdiv.style.height = defaultHeight + 'px';
    testdiv.style.fontSize = '30px';
    testdiv.style.textAlign = 'center';
    testdiv.innerHTML = iCount;
    testdiv.style.top = '500px';
    testdiv.style.position = 'absolute';
    iCount++;
    return testdiv;
}

document.getElementById('go').onclick = function() {
    var testdiv = createBlock();

    testdiv.style.transition = "top 2.0s linear 0s";

    setTimeout(function() {
        testdiv.style.top = (defaultHeight*-2) + 'px';
    }, 0); // <- change this to a higher value to see the transition always occur
};

When the "go" button (see JSBin) is clicked rapidly the div only appears sporadically (presumably due to the timing issue described above).

If you increase the setTimeout's delay value you can see the transition almost always work.

Is there a way to deterministically kick off a transition immediately after creating an element (without having to resort to a delay)?

Jonathan.Brink
  • 23,757
  • 20
  • 73
  • 115
  • You need to add the animated (new) top value in a class and assign that class instead, or else the transition doesn't know between which top to animate since the latter value overwrites the former. – Asons May 23 '16 at 16:13
  • @LGSon The newly created div is given an initial `top` value of '500px'...why would that need to be defined in a CSS class (other than as a best-practice)? – Jonathan.Brink May 23 '16 at 16:36
  • Misread your question a little ... will post an answer in a sec. – Asons May 23 '16 at 16:51

2 Answers2

5

For a transition you need two distinct states aka. a change.

Between two render-cycles styles are only overwritten. They are only applied when (re-)rendering the node.
So, if your setTimeout() fires before the "old" styles have been applied, the stlyes are only overwritten, and your Nodes are rendered with the target-style.

Afaik. most (desktop)-browsers strive for a 60fps framerate wich makes a 16.7ms interval. So setTimeout(fn,0) will most likely fire before that.

You can either increase the timeout as you mentioned (I would recommend at least 50ms), or you can trigger/enforce the rendering of the node; for example by asking for it's size.

To get the size of the Node, the browser first has to apply all styles to the Node, to know how they influence it.
Also, context matters for css, so the Node has to be added somewhere to the DOM, for the browser to be able to get the finally computed styles.

Long answer short:

document.getElementById('go').onclick = function() {
    var testdiv = createBlock();
    testdiv.style.transition = "top 2.0s linear 0s";

    testdiv.scrollWidth;  //trigger rendering

    //JsBin brags that you don't do anything with the value.
    //and some minifyer may think it is irrelevant and remove it.
    //so, increment it (`++`); the value is readonly, you can do no harm by that.
    //but the mere expression, as shown, already triggers the rendering.

    //now set the target-styles for the transition
    testdiv.style.top = (defaultHeight*-2) + 'px';
};
Thomas
  • 3,513
  • 1
  • 13
  • 10
  • That's a behaviour you can intentionally utilize like in this case, or it can have some significant performance-impact on your code, just simply by the order you do things in your code. Because in terms of perfomance, the most expensive thing you can do is triggering an unnecessary re-rendering; by changing some className on a node, and then asking for anything influenced by the style on one of the child-nodes. like the position, for example. – Thomas May 23 '16 at 17:34
  • So what you are saying is, in this case, using `animation` will gain performance against trigger a re-render using `transition` ? – Asons May 23 '16 at 17:37
  • No, I'm saying, that you should keep that behaviour in mind, especially when you're looking for performance-issues. That we often trigger unnecessary re-renderings just by the order we do things in our code. Most of the time we don't care (much) about performance in the frontend, but this is a good point to start with when you're looking for unnecessary re-renderings. – Thomas May 23 '16 at 17:42
  • any way to force the `setTimeout` callback to the next rendering cycle without using promises if thats even what promises do? the problem im running into is that im transitioning an `after` pseudo element via adding a class with JS – oldboy Nov 14 '20 at 11:42
1

An alternative is to use animation

If you want to create the keyframes dynamically, using javascript, here is a nice post showing that: https://stackoverflow.com/questions/10342494/set-webkit-keyframes-values-using-javascript-variable

var
    defaultHeight = 50,
    iCount = 0,
    aColors = ['red', 'orange', 'yellow', 'green', 'blue', 'purple'];

function createBlock() {
    var testdiv = document.createElement('div');
    testdiv.className = 'testdiv';
    document.body.appendChild(testdiv);
    testdiv.style.left = '50%';
    testdiv.style.backgroundColor = aColors[iCount % aColors.length];
    testdiv.style.width = defaultHeight + 'px';
    testdiv.style.height = defaultHeight + 'px';
    testdiv.style.fontSize = '30px';
    testdiv.style.textAlign = 'center';
    testdiv.innerHTML = iCount;
    testdiv.style.top = '300px';
    testdiv.style.position = 'absolute';
    // you can preset the animation here as well
    //testdiv.style.animation = 'totop 2.0s linear forwards';
    iCount++;
    return testdiv;
}

document.getElementById('go').onclick = function() {
    var testdiv = createBlock();
    testdiv.style.animation = 'totop 2.0s linear forwards';
};
@keyframes totop {
  100% { top: -100px; }
}
<button id="go">go</button>
Community
  • 1
  • 1
Asons
  • 84,923
  • 12
  • 110
  • 165
  • This looks good but the use of animations requires defining the keyframes which needs to be set with CSS. I am looking for a all-JS solution where I don't have to insert anything new into the document's styles. I understand this is non-standard. Do you know why animations are working here but not transitions? – Jonathan.Brink May 23 '16 at 17:06
  • @Jonathan.Brink The animation have both a start and an end value when created and transition not, hence transition needs to be drawn first, before assigning it a second top value, which kicks of the transition. The delay in your case does make that happen. – Asons May 23 '16 at 17:14
  • ...ah "the transition needs to be drawn first" may be the key to understanding the problem...what do you mean "needs to be drawn first"? Are you talking about a step that happens prior to the transition actually being kicked off (by the style change)? – Jonathan.Brink May 23 '16 at 17:15
  • @Jonathan.Brink Thomas just posted an answer which describes it well. – Asons May 23 '16 at 17:17
  • @Jonathan.Brink Updated my answer with a link, to fulfill your requirement on how to create the `keyframes` using script – Asons May 23 '16 at 17:27