Javascript Add/remove Animation Class, Only Animates Once
Solution 1:
I think this is how you would do in in vanilla
gugateider is right, your gonna need a setTimout, there is AnimationEvent API but its not fully supported.
below is a streamlined approach:
no event listners or collecting IDs needed
updated:
- Included a debounce to stop mulitple clicks to the same button
- tweaked timeout (The timeout is added so that the value changes out of view to give the illusion the numbers are actually rotating, the AnimationEvent cant detect if you are half way through your animation.)
<div class="wrapper">
<div id="hour" class="unit">
<div class="plus button" onClick="pressedButton(event);">+</div>
<div class="value"><div>10</div></div>
<div class="minus button" onClick="pressedButton(event);">-</div>
</div>
<div id="minute" class="unit">
<div class="plus button" onClick="pressedButton(event);">+</div>
<div class="value"><div>36</div></div>
<div class="minus button" onClick="pressedButton(event);">-</div>
</div>
<div id="meridiem" class="unit">
<div class="plus button" onClick="pressedButton(event);">+</div>
<div class="value"><div>AM</div></div>
<div class="minus button" onClick="pressedButton(event);">-</div>
</div>
</div>
.wrapper {
display: flex;
flex-flow: row no-wrap;
justify-content: space-between;
align-items: center;
width: 200px;
height: 200px;
margin: 100px auto;
background: red;
padding: 20px;
}
.unit {
display: flex;
flex-flow: column;
justify-content: space-between;
align-items: space-between;
height: 100%;
position: relative;
}
.button {
border: 2px solid black;
height: 50px;
width: 50px;
cursor: pointer;
display: flex;
justify-content: center;
align-items: center;
background: grey;
&:hover {
opacity: 0.8;
}
}
.value {
border: 2px solid black;
height: 50px;
width: 50px;
font-family: sans-serif;
font-size: 20px;
display: flex;
justify-content: center;
align-items: center;
background: lightgrey;
overflow: hidden;
}
.animate {
top: 0;
position: relative;
overflow: hidden;
animation-name: downandout;
animation-duration: 1s;
animation-iteration-count: 1;
transition-timing-function: ease-in-out;
&--reverse {
animation-direction: reverse;
}
}
@keyframes downandout {
0% {top: 0}
50% {top: 50px}
51% {top: -50px}
100% {top: 0}
}
debounce = function(func, wait, immediate) {
var timeout;
return function() {
var context = this,
args = arguments;
var later = function() {
timeout = null;
if (!immediate) func.apply(context, args);
};
var callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) func.apply(context, args);
};
}
pressedButton = function($event) {
$event.preventDefault();
// debouncedFn to stop multiple clicks to the same button
var debouncedFn = debounce(function() {
// All the taxing stuff you do
const target = $event.target;
const elm = target.parentNode.children[1];
let direction;
// clone the element to restart the animation
const newone = elm.cloneNode(true);
// add the first animate
newone.children[0].classList.add("animate");
// What button was pressed
if (target.classList.contains("minus")) {
direction = "down";
} else {
direction = "up";
}
// direction of animation
if (direction === "down") {
newone.children[0].classList.add("animate--reverse");
} else {
newone.children[0].classList.remove("animate--reverse");
}
// add the new element to the DOM
elm.parentNode.replaceChild(newone, elm);
// change value after half of the animation has completed
setTimeout(function() {
switch (target.parentNode.id) {
case "hour":
case "minute":
if (direction === "down") {
newone.children[0].innerText--;
} else {
newone.children[0].innerText++;
}
break;
case "meridiem":
if (newone.children[0].innerText === "PM") {
newone.children[0].innerText = "AM";
} else {
newone.children[0].innerText = "PM";
}
}
}, 100);
}, 250);
// Call my function
debouncedFn();
};
Solution 2:
You can use animation iteration event listener to detect when your animation has ended, and correctly remove your class. for this to work, animation iteration has to be set to infinite. in my changes, i also took advantage of the classList
property of elements to add/remove the class from the element.
NOTE: without atleast 1 iteration, the animation iteration event will never fire, and thus the code will not work!
EDIT: browser support for classList
is fairly recent, so if you need to support older browsers you can fall back to a different solution, or add a classList polyfill
first we, need a function to detect which animation iteration event is supported by the browser:
function whichAnimationEvent(){
var el = document.createElement("fakeelement");
var animations = {
"animation" : "animationiteration",
"OAnimation" : "oAnimationIteration",
"MozAnimation" : "animationiteration",
"WebkitAnimation": "webkitAnimationIteration"
};
for (let t in animations){
if (el.style[t] !== undefined){
return animations[t];
}
}
}
next, we need to add an event listener for that event, and when the iteration fires, we remove the class from the element's classList
, as shown below:
ampmEl.addEventListener(whichAnimationEvent(),function(){
console.log('ampmEl event listener fired')
ampmEl.classList.remove('animateStart');
});
next, we change the swapAMPM
function to use the add
method of the elements classList
to add the class before performing the swap, so that it is animated.
function swapAMPM() {
let value = ampmEl.innerHTML;
ampmEl.classList.add('animateStart');
if (value === "AM") {
value = "PM";
ampmEl.innerHTML = value;
console.log("Changed from AM");
} else {
value = "AM";
ampmEl.innerHTML = value;
console.log("Changed from PM");
}
}
finally, we need to update the css to have infinite
animation-iteration, so that our event will fire.
.animateStart {
-webkit-animation-name: downandout; /* Safari 4.0 - 8.0 */
animation-name: downandout;
-webkit-animation-duration: 250ms; /* Safari 4.0 - 8.0 */
animation-duration: 250ms;
-webkit-animation-iteration-count: infinite;
animation-iteration-count: infinite;
transition-timing-function: cubic-bezier(1,0,0,1);
}
full working example
Solution 3:
I know setTimeout is not the best option to use in there, but since you're already pretty much done with that you can try replacing the swapAMPM function with the code below:
function swapAMPM() {
let value = ampmEl.innerHTML;
if (ampmEl.hasAttribute("class")) {
ampmEl.removeAttribute("class");
}
setTimeout( () => {
ampmEl.setAttribute("class", "animateStart");
if (value === "AM") {
value = "PM";
ampmEl.innerHTML = value;
console.log("Changed from AM");
} else {
value = "AM";
ampmEl.innerHTML = value;
console.log("Changed from PM");
}
}, 150);
}
Solution 4:
in your function swapAMPM
in the if statement
, you try to remove an atribute, which doesn't make much cense, because you immediately sett it back..
if (ampmEl.hasAttribute("class")) {
ampmEl.removeAttribute("class");
}
ampmEl.setAttribute("class", "animateStart")
A quick fix would be to set a timeout to remove the class right after the animation is finished:
function swapAMPM() {
let value = ampmEl.innerHTML;
ampmEl.classList.add("animateStart");
setTimeout(() => {
ampmEl.classList.remove("animateStart")
}, 260)
if (value === "AM") {
value = "PM";
ampmEl.innerHTML = value;
console.log("Changed from AM");
} else {
value = "AM";
ampmEl.innerHTML = value;
console.log("Changed from PM");
}
}
Post a Comment for "Javascript Add/remove Animation Class, Only Animates Once"