Trying to build a time picker, that is supposed to animate the change of input. I thought I'd do this by adding and removing an animation class from CSS. The problem is, that it only works the first time.
I tried to set an interval to start with, that removed the class after a certain amount of time, slightly above the animation duration, but that reduced the animation time of the animation with each click, and by the third or fourth click, it no longer animated.
Then I just opted to check if the class existed to start with, remove it to start with, then add it. To make sure it starts from scratch each time. But now it only animates the very first time.
Codepen
HTML:
<div id="wrapper">
<div id="uphour"></div>
<div id="upminute"></div>
<div id="upampm"></div>
<div class="animated">
<div id="hour" data-hour="1">2</div>
</div>
<div class="animated">
<div id="minute" data-minute="1">50</div>
</div>
<div class="animated">
<div id="ampm" data-minute="AM">AM</div>
</div>
<div id="downhour"></div>
<div id="downminute"></div>
<div id="downampm"></div>
</div>
(the data attributes here are not currently in use)
SCSS:
$finger: 2cm;
$smallfinger: .3cm;
#wrapper {
display: grid;
grid-template-columns: $finger $finger $finger;
grid-template-rows: $finger $finger $finger;
grid-column-gap: $smallfinger;
grid-row-gap: $smallfinger;
position:absolute;
width: 100vw;
background: #FF00FF;
height: calc( (#{$finger} * 3) + (#{$smallfinger} * 4) );
box-sizing: border-box;
bottom:$finger;
left:0;
padding:0;
margin:0;
align-content: center;
justify-content: center;
}
#uphour,
#upminute,
#upampm,
#downhour,
#downminute,
#downampm {
display: flex;
background: rgba(255,0,0,.4);
width: $finger;
height: $finger;
margin-left: 1vw;
margin-right: 1vw;
border: 1px solid black;
}
.animated {
display: flex;
width: $finger;
height: $finger;
margin-left: 1vw;
margin-right: 1vw;
border: 1px solid black;
overflow:hidden;
}
#minute, #hour, #ampm {
display: flex;
/*background: rgba(255,0,0,.4); for testing of animation only*/
width: $finger;
height: $finger;
border: 0;
position: relative;
align-items: center;
justify-content: center;
font-size: .8cm;
}
.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);
}
/* Safari 4.0 - 8.0 */
@-webkit-keyframes downandout {
0% {top: 0; left: 0;}
10% {top: 100%; left:0;}
50% {top: 100%; left: 100%;}
90% {top: -100%; left:100%;}
}
/* Standard syntax */
@keyframes downandout {
0% {top: 0; left: 0;}
40% {top: 100%; left:0;}
42% {top: 100%; left: calc(#{$finger} * 2);}
48% {top: -110%; left: calc(#{$finger} * 2);}
50% {top: -110%; left:0;}
100% {top: 0; left:0;}
}
Javascript:
var uphourEl = document.getElementById("uphour");
var downhourEl = document.getElementById("downhour");
var upminuteEl = document.getElementById("upminute");
var downminuteEl = document.getElementById("downminute");
var upampmEl = document.getElementById("upampm");
var downampmEl = document.getElementById("downampm");
uphourEl.addEventListener("click", increaseHour);
downhourEl.addEventListener("click", decreaseHour);
upminuteEl.addEventListener("click", increaseMinute);
downminuteEl.addEventListener("click", decreaseMinute);
upampmEl.addEventListener("click", swapAMPM);
downampmEl.addEventListener("click", swapAMPM);
var hourEl = document.getElementById("hour");
var minuteEl = document.getElementById("minute");
var ampmEl = document.getElementById("ampm");
function increaseHour() {
let value = hourEl.innerHTML;
if (value > 11) {
value = 1
} else {
value++;
}
hourEl.innerHTML = value;
console.log(value);
}
function decreaseHour() {
let value = hourEl.innerHTML;
if (value < 2) {
value = 12
} else {
value--;
}
hourEl.innerHTML = value;
console.log(value);
}
function increaseMinute() {
let value = minuteEl.innerHTML;
if (value > 58) {
value = 0
} else {
value++;
}
minuteEl.innerHTML = value;
console.log(value);
}
function decreaseMinute() {
let value = minuteEl.innerHTML;
if (value < 1) {
value = 59
} else {
value--;
}
minuteEl.innerHTML = value;
console.log(value);
}
function swapAMPM() {
let value = ampmEl.innerHTML;
if (ampmEl.hasAttribute("class")) {
ampmEl.removeAttribute("class");
}
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");
}
}
..it's the swapAMPM function (at the end of the Javascript) I'm currently not getting to behave as expected.
Any suggestions on why I am experiencing this behavior? Is there some best practice I am missing?
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
codepen
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
<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();
};
https://codepen.io/eddy14u/pen/VwZJmdW
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With