Skip to content Skip to sidebar Skip to footer

Javascript Add/remove Animation Class, Only Animates Once

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 on

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();

};

https://codepen.io/eddy14u/pen/VwZJmdW


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

codepen


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"