Question about execution sequence with toggling CSS attributes (namely animation) [duplicate] - javascript

This question already has answers here:
Restart animation in CSS3: any better way than removing the element?
(14 answers)
Closed 1 year ago.
So in this example, if I don't nest the
box.style.animation = 'myanimation 3s'
inside of the setTimeout, then the code doesn't work. But when I run it like this, with a timeout delay of ZERO, it works. Originally I thought that maybe it was an execution timing error, so I set the Timeout delay to 50, but I got curious and tried lower amounts until I got to zero, and it still ran. Essentially I was just trying to implement an animation every time the element is clicked. Is there a better/safer workaround for this? Also, for curiosity, what is the difference between running that line of code directly after the 'if' statement versus nesting it in a setTimeout with a delay of 0? I'm using Firefox with Ubuntu LTS 20.04
edit: I should add, if I don't nest inside the setTimeout function, the animation will run the first time, but not any subsequent time. But when I run the code as shown, it runs everytime.enter image description here
const box = document.querySelector('.box')
box.addEventListener('click', function() {
if(box.style.animation){
box.style.animation = ''
}
setTimeout(function() {
box.style.animation = 'myanimation 3s'
}, 0)
})
EDIT: Another answer I came across on reddit, is to listen for an animationend event, such as:
const box = document.querySelector('.box')
box.addEventListener('click', function() {
box.style.animation = 'myanimation 3s'
})
box.addEventListener('animationend', function() {
box.style.animation = ''
})

The reason why it seems like nothing is happening when you don't use the timeout is that modern browsers optimize away the first statement completely, in order to prevent unnecessary operations from happening that would have a negative effect on performance.
The browser notices that you set the animation property to an empty string, just to set it to another value in the line below. That would cause a reflow, and in many cases that wouldn't be what you want. In this case, trying to reset a CSS animation, it is exactly what you want, though.
So you need to tell the browser to intentionally cause a reflow. One way to do that is using setTimeout, so the first expression will not be optimized away. Another way is to cause reflow by doing something that will itself inherently cause a reflow. offsetLeft, offsetTop, offsetWidth and offsetHeight are such a properties that cause a reflow in order to report back accurate values when they are accessed. It's enough to simply access them, without putting the values into a variable or anything:
const box = document.querySelector('.box')
box.addEventListener('click', function() {
if (box.style.animation) {
box.style.animation = ''
}
box.offsetLeft; // this forces a reflow, just like setTimeout
box.style.animation = 'myanimation 3s'
})
.box {
background: red;
width: 120px;
height: 120px;
position: relative;
}
.box::after {
content: "click me!";
color: #fff;
display: block;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%)
}
#keyframes myanimation {
from {
transform: translateY(0)
}
50% {
transform: translateY(1em)
}
to {
transform: translateY(0);
}
}
<div class="box">
</div>

Related

How is it possible to transition the position of items in a list without using transform or top/left

The other day I stumbled onto an example that uses Vue.js, but my question is more about the CSS and HTML that Vue uses to achieve the transition between states.
The cards temporarily get the class .shuffleMedium-move which adds a transition: transform 1s and the order of the nodes change in the DOM, but I don't understand why the transition occurs since the transform property never seems to get set and the items are positioned simply using float:left.
I've been doing CSS for quite a while and I've always had to resort to using a combination of JavaScript position: absolute and transform to achieve a similar result. Vue's solution seems really elegant, but I don't understand how it works.
From the documentation on list transition
This might seem like magic, but under the hood, Vue is using an animation technique called FLIP to smoothly transition elements from their old position to their new position using transforms.
From the FLIP article
FLIP stands for First, Last, Invert, Play.
Let’s break it down:
First: the initial state of the element(s) involved in the transition.
Last: the final state of the element(s).
Invert: here’s the fun bit. You figure out from the first and last how the element has changed, so – say – its width, height,
opacity. Next you apply transforms and opacity changes to reverse, or
invert, them. If the element has moved 90px down between First and
Last, you would apply a transform of -90px in Y. This makes the
elements appear as though they’re still in the First position but,
crucially, they’re not.
Play: switch on transitions for any of the properties you changed, and then remove the inversion changes. Because the element or
elements are in their final position removing the transforms and
opacities will ease them from their faux First position, out to the
Last position.
Step by step example
That way, we can inspect changes at each step of the animation process.
When it's playing in real time, the transform is really quickly added inline and it's then removed immediately, so it looks like it's never set.
const el = document.getElementById('target');
const data = {};
function first() {
data.first = el.getBoundingClientRect();
console.log('First: get initial position', data.first.left, 'px');
}
function last() {
el.classList.toggle('last');
data.last = el.getBoundingClientRect();
console.log('Last: get new position', data.last.left, 'px');
}
function invert() {
el.style.transform = `translateX(${data.first.left - data.last.left}px)`;
console.log('Invert: applies a transform to place the item where it was.');
}
function play() {
requestAnimationFrame(() => {
el.classList.add('animate');
el.style.transform = '';
});
console.log('Play: adds the transition class and removes the transform.');
}
function end() {
el.classList.remove('animate');
console.log('End: removes the transition class.');
}
const steps = [first, last, invert, play, end];
let step = 0;
function nextStep() {
steps[step++ % steps.length]();
}
document.getElementById('next').addEventListener('click', nextStep);
.last {
margin-left: 35px;
}
.animate {
transition: transform 1s;
}
#target {
display: inline-block;
padding: 5px;
border: 1px solid #aaa;
background-color: #6c6;
}
#next {
margin-top: 5px;
}
<div id="target">target</div>
<br>
<button id="next" type="button">Next</button>

Get CSS value mid-transition with native JavaScript

This question was asked before, but the answer uses jQuery, here.
So, I am going to tweak the question to specifically ask for a native solution, to minimize dependencies.
Let's say hypothetically, you have a <div> and that <div> is in mid-transition of its opacity value and top value. How would I get the value of both of those properties mid-transition using native JavaScript?
It is very easy to port the jQuery script from the linked thread into its vanilla JavaScript equivalent and below is a sample. The output is printed on the right side (output#op element) once timer expires.
All that we are doing is the following:
Attach two event handlers to the element which triggers the transition (sometimes the triggering element can be different from the one that has animation). In the other thread, the element that is triggering the transition and the one that is being transitioned is the same. Here, I have put it on two different elements just for a different demo.
One event handler is for mouseover event and this creates a timer (using setTimeout) which gets the opacity and top value of the element that is being transitioned upon expiry of timer.
The other event handler is for mouseleave event to clear the timer when the user has hovered out before the specific point at which we need the opacity and top value to be obtained.
Getting the opacity and top value of the element that is being transitioned can be obtained by using the window.getComputedStyle method.
Unlike the demo in the other thread (which uses setInterval), here I have used setTimeout. The difference is that setInterval adds an interval and so the function is executed every x seconds whereas the function passed to setTimeout is executed only once after x seconds. You can use whichever fits your needs.
var wrap = document.querySelector('.wrapper'),
el = document.querySelector('.with-transition'),
op = document.querySelector('#op');
var tmr;
wrap.addEventListener('mouseenter', function() {
tmr = setTimeout(function() {
op.innerHTML = 'Opacity: ' + window.getComputedStyle(el).opacity +
', Top: ' + window.getComputedStyle(el).top;
}, 2500);
});
wrap.addEventListener('mouseleave', function() {
clearTimeout(tmr);
});
.wrapper {
position: relative;
height: 400px;
width: 400px;
background: yellowgreen;
}
.with-transition {
position: absolute;
top: 0px;
left: 100px;
width: 200px;
height: 100px;
background: yellow;
opacity: 0;
transition: all 5s linear;
}
.wrapper:hover .with-transition {
top: 300px;
opacity: 1;
}
output {
position: absolute;
top: 50px;
right: 50px;
}
<div class='wrapper'>
<div class='with-transition'></div>
</div>
<output id='op'></output>
The answer referenced in the duplicate question is easily modified to NOT use jquery. There is no black magic happening there.
The real question is why would you want to do this?
If You need control over a transition just impliment the partial transition with javascript, do what you need, then complete the transition.

"animationend" does not fire sometimes

I have a CSS animation that gets applied to a HTML element with a CSS class. I also have an event listener attached to the animationend (it's a modern Windows app so the browser is IE11 only). What I'm seeing is that sometimes the event gets fired and sometimes it doesn't. Regardless of the event firing I can always see it visually animating. It looks like some kind of race condition to me.
I have searched the web trying to understand what could cause this event to not get fired but I haven't found any satisfying results. I found the following on MDN:
Note: The transitionend event doesn't fire if the transition is aborted because the animating property's value is changed before the transition is completed.
UPDATE1: transitionend has nothing to do with animationend so this info is unrelated.
I'm not sure if the same is applicable for CSS animations. Does anyone have any ideas as to what can cause this? If there was any event that could detect that an animation was aborted that could also be a useful workaround.
UPDATE2: The current workaround that I'm using:
element.addEventListener("animationstart", function () {
setTimeout(function () {
// do stuff
}, animationDuration);
});
Is it possible that your animations aren't ending?
Check the documentation for the animationcancel event:
https://developer.mozilla.org/en-US/docs/Web/Events/animationcancel
However, it looks like browser support may be spotty ("onanimationcancel" in HTMLElement.prototype returns false for me in Chrome 67).
It seems that using a setTimeout hack may be necessary for the time being.
You can use this function
function onAnimationEnd(element, handler) {
//- Create local variables
var events = 'animationend transitionend webkitAnimationEnd oanimationend MSAnimationEnd webkitTransitionEnd otransitionend oTransitionEnd msTransitionEnd';
//- Bind event to element
$(element).on(events, handler)
//- Return received element
return element
}
This function binds an event when both animation or transition ends.
Also make sure that the animated element is not being deleted or moved while the animation is running.
With IE11, you can't necessarily assume that the JS event binding will occur before the CSS animation starts or ends. This type of issue is intermittent, and frequency/onset will depend on certain factors.
If the CSS animation-duration is low and the animation starts immediately after the stylesheet is ready, then it's possible that the JS event binding will not be applied in time
A solid workaround I've found is to use JS to invoke the CSS animation using a className. i.e., Only apply the CSS animation-name property when the className has been applied via JS.
FWIW, this is still a problem. In some cases (so far I've only reproduced it on mobile) certain elements with a well-defined animation don't always fire the animationend event.
EDIT: Turns out the issue is with animating after content. Safari just doesn't reliably handle it. I ended up making a distinct strikethrough div and animating that.
function animateCSS(element, animationName, callback) {
const node = typeof element == 'string' ? document.querySelector(element) : element;
node.classList.add('animated', animationName);
node.onanimationend = handleAnimationEnd;
console.log('animateCSS', node, animationName);
function handleAnimationEnd() {
console.log('handleAnimationEnd', node, 'remove:', animationName);
node.classList.remove('animated', animationName);
node.onanimationend = null;
if (typeof callback === 'function') callback(node);
}
}
.strike {
color: #e402b3;
position: relative;
}
.strike:after {
content: ' ';
position: absolute;
top: 50%;
left: 2%;
height: 2px;
background: #e402b3;
animation: strike 0.7s linear 0s 1 forwards;
-webkit-animation: strike 0.7s linear 0s 1 forwards;
}
#keyframes strike {
0% {
width: 0%;
color: #000;
}
100% {
width: 96%;
color: #e402b3;
}
}
#-webkit-keyframes strike {
0% {
width: 0%;
color: #000;
}
100% {
width: 96%;
color: #e402b3;
}
}

Why is setTimeout needed when applying a class for my transition to take effect?

I have an element with a transition applied to it. I want to control the transition by adding a class to the element which causes the transition to run. However, if I apply the class too quickly, the transition effect does not take place.
I'm assuming this is because the .shown is placed onto the div during the same event loop as when .foo is placed onto the DOM. This tricks the browser into thinking that it was created with opacity: 1 so no transition is put into place.
I'm wondering if there is an elegant solution to this rather than wrapping my class in a setTimeout.
Here's a snippet:
var foo = $('<div>', {
'class': 'foo'
});
foo.appendTo($('body'));
setTimeout(function(){
foo.addClass('shown');
});
.foo {
opacity: 0;
transition: opacity 5s ease;
width: 200px;
height: 200px;
background-color: red;
}
.foo.shown {
opacity: 1;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
Actually, the point is not about the setTimeout, but about how the element is rendered.
The CSS transition will only appear if the element is rendered with a property value, and then this property is changed.
But once you append the element, it does not mean that it was rendered. Simply adding a setTimeout is not enough. Thought it may work for you, in some browser versions it won't work! (Mostly Firefox)
The point is about the element's render time. Instead of setTimeout, you can force a DOM render by requesting a visual style property, and then changing the class:
var foo = $('<div>', {
'class': 'foo'
});
foo.appendTo($('body'));
//Here I request a visual render.
var x = foo[0].clientHeight;
//And it works, without setTimeout
foo.addClass('shown');
.foo {
opacity: 0;
transition: opacity 5s ease;
width: 200px;
height: 200px;
background-color: red;
}
.foo.shown {
opacity: 1;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
When you do DOM manipulation that that javascript relies on immediately afterwards, you need to pause javascript execution briefly in order to allow rendering to catch up, since that will be done asynchronously. All a blank setTimeout does is move the code within to the end of the current execution pipeline. The browser must complete rendering the new layout before it will obey a trigger for your transition so the setTimeout is a good idea and in my opinion the most elegant solution.

Using addClass() and CSS transition on render function in Backbone app not working correctly

In my backbone.js application, I'm trying to fade in the view element after it's been appended. However it doesn't work.
Live example here: http://metropolis.pagodabox.com
var itemRender = view.render().el;
$('#items-list').append(itemRender);
$(itemRender).addClass('show');
However if I add a small setTimeout function, it works.
var itemRender = view.render().el;
$('#items-list').append(itemRender);
setTimeout(function(){
$(itemRender).addClass('show');
},10);
Using fadeIn() also works but I prefer to use straight CSS for the transition as it's more efficient, and prefer not to use any setTimeout "hacks" to force it to work. Is there a callback I can use for append? Or any suggestions? The full code is below:
itemRender: function (item) {
var view = new app.ItemView({ model: item }),
itemName = item.get('name'),
itemRender = view.render().el;
$('#items-list').append(itemRender);
$(itemRender).addClass('show');
app.itemExists(itemName);
}
CSS/LESS:
#items-list li {
padding: 0 10px;
margin: 0 10px 10px;
border: 1px solid #black;
.border-radius(10px);
position: relative;
.opacity(0);
.transition(opacity)
}
#items-list li.show {.opacity(1)}
This "hack" you mention (or some variant of it) is occasionally necessary for web development, simply due to the nature of how browsers render pages.
(NOTE: This is all from memory, so while the overall idea is right please take any details with a small grain of salt.)
Let's say you do the following:
$('#someElement').css('backgroundColor', 'red');
$('#someElement').css('backgroundColor', 'blue');
You might expect to see the background color of #someElement flash red for a brief moment, then turn blue right? However, that won't happen, because browsers try to optimize rendering performance by only rendering the final state at the end of the JS execution. As a result, the red background will never even appear on the page; all you'll ever see is the blue.
Similarly here, the difference between:
append
set class
and:
append
wait 1ms for the JS execution to finish
set class
Is that the latter allows the element to enter the page and AFTER the JS is executed have its style change, while the former just applies the style change before the element gets shown.
So while in general window.setTimeout should be avoided, when you need to deal with these ... complications of browser rendeering, it's really the only way to go. Personally I like using the Underscore library's defer function:
var itemRender = view.render().el;
$('#items-list').append(itemRender);
_(function(){
$(itemRender).addClass('show');
}).defer();
It's the same darn thing, but because it's encapsulated in a library function it feels less dirty to me :-) (and if the "post-render" logic is more than a line or two I can factor it in to a Backbone View method and do _(this.postRender).defer() inside my render method).
You can use CSS animations
#keyframes show {
0% { opacity: 0; }
100% { opacity: 1; }
}
#items-list li {
padding: 0 10px;
margin: 0 10px 10px;
border: 1px solid #black;
.border-radius(10px);
position: relative;
}
#items-list li.show {
animation: show 1s;
}

Categories