0

I'm trying to create a cool little micro interaction, but I'm running into a minor issue.

document.querySelector('button').onclick = function(){
  const
    items = document.querySelector('nav').children
  if (items[0].getBoundingClientRect().top >= document.querySelector('nav').getBoundingClientRect().bottom){
    // start showing elements, starting from the left side
    for (let i = 0; i < items.length; i++){
      setTimeout(function(){
        items[i].style.transform = 'translateY(0)'
      }, i * 200)
    }
  } else {
    // start hiding elements, starting from the right side
    for (let i = 0; i < items.length; i++){
      setTimeout(function(){
        items[i].style.transform = 'translateY(100%)'
      }, (items.length-1 - i) * 200)
    }
  }
}
button {
  width: 100px;
  height: 50px;
}

nav {
  width: 50vw;
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  grid-gap: 10px;
  background: red;
}

nav > a {
  width: 100%;
  height: 50px;
  transition: .5s transform;
  transform: translateY(100%);
  opacity: 0.5;
  background: lime;
}
<button>Toggle</button>
<nav>
  <a href=''></a>
  <a href=''></a>
  <a href=''></a>
  <a href=''></a>
</nav>

If you toggle in too quick of a succession, some items will ultimately be displayed, whereas others will ultimately be hidden.

This is due to the fact that there are pending setTimeouts that have yet to be executed when the new set of setTimeouts are issued.

Obviously there are ways around this issue, like not reversing the order of the animation, waiting until the animation is completely finished before allowing the reverse, et cetera, but I would rather not make such compromises.

I've tried using and toggling a global Boolean in the if and else blocks, and then using an additional if/else statement in the setTimeout block, but this didn't work.

I also tried setting transition delays on the fly before applying the new transform values, instead of relying on setTimeout, which didn't work.

Is there a simple way to cancel or ignore any pending setTimeouts from the older cycle?

oldboy
  • 5,729
  • 6
  • 38
  • 86
  • What is supposed to do? Click toggle button...nothing happens. Click one of the links and it goes nowhere of course. – zer00ne May 14 '19 at 22:50
  • @zer00ne not sure why its not working for u? works for me and im assuming worked for the other buddy who already provided an answer? – oldboy May 14 '19 at 22:51
  • @zer00ne if for whatever reason u still cant get it to work, heres a [codepen](https://codepen.io/tOkyO1/details/gJgrYe) – oldboy May 14 '19 at 23:00

2 Answers2

2

I would simplify your logic and consider transition-delay where you only need to toggle a class. The trick is to have a different delay for your elements when we toggle the class to have the desired effect.

With this configuration you won't have any issue because all the element will have the same state since the class is added to their parent element.

var nav = document.querySelector('nav');
document.querySelector('button').onclick = function(){
  nav.classList.toggle('top');
}
button {
  width: 100px;
  height: 50px;
}

nav {
  width: 50vw;
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  grid-gap: 10px;
  background: red;
  --d:0.2s;
}

nav > a {
  width: 100%;
  height: 50px;
  transition: .5s transform;
  transform: translateY(100%);
  opacity: 0.5;
  background: lime;
}
nav.top > a {
  transform: translateY(0);
}

nav > a:nth-last-child(1) { transition-delay:calc(0 * var(--d));}
nav > a:nth-last-child(2) { transition-delay:calc(1 * var(--d));}
nav > a:nth-last-child(3) { transition-delay:calc(2 * var(--d));}  
nav > a:nth-last-child(4) { transition-delay:calc(3 * var(--d));}

nav.top > a:nth-child(1) { transition-delay:calc(0 * var(--d));}
nav.top > a:nth-child(2) { transition-delay:calc(1 * var(--d));}
nav.top > a:nth-child(3) { transition-delay:calc(2 * var(--d));}  
nav.top > a:nth-child(4) { transition-delay:calc(3 * var(--d));}
<button>Toggle</button>
<nav>
  <a href=''></a>
  <a href=''></a>
  <a href=''></a>
  <a href=''></a>
</nav>

We can simplify the CSS code by grouping the elements with the same delay:

var nav = document.querySelector('nav');
document.querySelector('button').onclick = function(){
  nav.classList.toggle('top');
}
button {
  width: 100px;
  height: 50px;
}

nav {
  width: 50vw;
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  grid-gap: 10px;
  background: red;
  --d:0.2s;
}

nav > a {
  width: 100%;
  height: 50px;
  transition: .5s transform;
  transform: translateY(100%);
  opacity: 0.5;
  background: lime;
}
nav.top > a {
  transform: translateY(0);
}

nav > a:nth-last-child(1),
nav.top > a:nth-child(1) { transition-delay:calc(0 * var(--d));}

nav > a:nth-last-child(2),
nav.top > a:nth-child(2) { transition-delay:calc(1 * var(--d));}

nav > a:nth-last-child(3),
nav.top > a:nth-child(3){ transition-delay:calc(2 * var(--d));}  

nav > a:nth-last-child(4),
nav.top > a:nth-child(4){ transition-delay:calc(3 * var(--d));}
<button>Toggle</button>
<nav>
  <a href=''></a>
  <a href=''></a>
  <a href=''></a>
  <a href=''></a>
</nav>
Temani Afif
  • 245,468
  • 26
  • 309
  • 415
  • dude youre a fucking wizard. its funny cuz i just tried setting `transition-delays` before applying new `transform` values in JS, but i couldnt get it to work. i never thought my understanding of CSS was so bad until i started working on this project lol – oldboy May 15 '19 at 01:34
  • i actually just figured it out using JS too. so instead of applying the style directly in the `setTimeout`, calling another function from and after the timeout and then applying whether a boolean is true or false does the trick too. but your answer/approach is so much more eloquent so ill be using yours :D – oldboy May 15 '19 at 01:40
  • 2
    @Anthony yes better avoid JS as much as possible while you are at the early stage of a project. Keep JS for things that are really impossible with CSS. – Temani Afif May 15 '19 at 01:44
  • @Anthony you can even be better ;) there is no magic, it's all about *hard* working – Temani Afif May 15 '19 at 01:58
  • Good job!! This is so satisfying I can't stop clicking the button – Pablo May 15 '19 at 03:04
  • random question regarding something i was thinking about before. thought id ask you instead of posting a whole other question. is there any way to retrieve the index of an item in an HTML collection (i.e. ele.children)? – oldboy May 15 '19 at 06:07
  • exactly like that but is there no way to do it in vanilla JS other than looping the the collection? – oldboy May 16 '19 at 18:36
  • @Anthony no I don't think there is a vanilla JS for this. You may ask such question in the JS Chat Room to confirm this. – Temani Afif May 17 '19 at 01:38
  • @TemaniAfif [any idea?](https://stackoverflow.com/questions/56213803/bug-in-safari-ios-12-2-and-12-3?noredirect=1#comment99048688_56213803) – oldboy May 20 '19 at 03:53
  • @Anthony no sorry, I have no way to test this behavior – Temani Afif May 20 '19 at 08:15
  • @TemaniAfif any idea how to add a transition to the `height` of [this effect?](https://codepen.io/tOkyO1/pen/MdrZWL) ive tried using `flex` and `min-height`, but that didnt work either. i hate how you cant transition `height: auto`. – oldboy May 22 '19 at 20:41
  • nm figured it out thx to [this answer.](https://stackoverflow.com/a/8331169/7543162) can use `max-height` instead of `height` – oldboy May 22 '19 at 20:53
0

This anwer shows a good way to clear all setTimeout - just add it into each part of the if/else statement:

document.querySelector('button').onclick = function() {
    const
      items = document.querySelector('nav').children

    if (items[0].getBoundingClientRect().top >= document.querySelector('nav').getBoundingClientRect().bottom) {
      var id = window.setTimeout(() => {}, 0);
      while (id--) {
        window.clearTimeout(id);
      }
      // start showing elements, starting from the beginning
      for (let i = 0; i < items.length; i++) {
        setTimeout(function() {
          items[i].style.transform = 'translateY(0)'
        }, i * 200)
      }
    } else {
      var id = window.setTimeout(() => {}, 0);
      while (id--) {
        window.clearTimeout(id);
      }
        // start hiding elements, starting from the back
        for (let i = 0; i < items.length; i++) {
          setTimeout(function() {
            items[i].style.transform = 'translateY(100%)'
          }, (items.length - 1 - i) * 200)
        }
      }
    }
button {
  width: 100px;
  height: 50px;
}

nav {
  width: 50vw;
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  grid-gap: 10px;
  background: red;
}

nav>a {
  width: 100%;
  height: 50px;
  transition: .5s transform;
  transform: translateY(100%);
  opacity: 0.5;
  background: lime;
}
<button>Toggle</button>
<nav>
  <a href=''></a>
  <a href=''></a>
  <a href=''></a>
  <a href=''></a>
</nav>
Jack Bashford
  • 43,180
  • 11
  • 50
  • 79
  • this method, although it successfully "cancels" the set of "forward/show" transitions, isnt working when one tries to "cancel" the set of "backwards/hide" transitions – oldboy May 14 '19 at 22:47
  • does `var id...` basically state that "any `setTimeouts` that are set *in this block (?)* of code are identified as `id`"? – oldboy May 14 '19 at 22:50
  • Mr. B, what exactly does this do? Click the toggle button -- nothing happens just like OP. – zer00ne May 14 '19 at 22:56
  • 1
    @zer00ne Click the toggle button twice in quick succession and the tiles reset. – Jack Bashford May 14 '19 at 23:27
  • @JackBashford they only reset if initially theyre hidden. however, if initially theyre visible, they dont reset, but pause for a second and then continue hiding themselves?? – oldboy May 15 '19 at 00:18