0

I have the following function:

function deleteFood(index) {
    // animate
    document.getElementById(foodList[index].id).style.transform = 'translate(500px, 0)';

    // delete from airtable
    foodList[index].destroy();

    window.setTimeout(() => {
      // delete from state
      let newState = foodList.slice();
      newState.splice(index, 1);
      setFoodList(newState);
    }, 500);
}

Basically there's a list, and the user can click delete on every list entry. If you click delete on an entry, it animates and then disappears when it's deleted. So far, everything works. But if I click delete on two entries very fast, only the second one gets deleted.

Say we have 4 entries (0,1,2,3), and I click delete on 1 and 2, then the list still has 0,1,3. All entries are animated and called destroy upon.

I tried using promises to no avail. How can I fix this?

palaѕн
  • 72,112
  • 17
  • 116
  • 136
torquan
  • 356
  • 1
  • 6
  • Where does the `destroy()` function come from? – Zorzi Apr 01 '20 at 17:32
  • I'm using airtable as database for this, the destroy() function deletes the element there. I should have removed it from the code, it works and is not part of the problem. It's really only the code inside setTimeout that get's only executed for the second call. – torquan Apr 01 '20 at 17:43
  • When you update `foodList`, the old indexes are no longer valid. – Barmar Apr 01 '20 at 17:51

2 Answers2

0

As you noted, if you you click delete very quickly only the second is being removed and the reason for this is that the index variable is being overwritten before the Timeout fires which is why the second one is the only one being deleted.

You could remove the item immediately and then leave the timeout for just resetting the list afterward your animation is completed. Or, add a listener for transition has ended to delete the item from the list instead of waiting on the timeout. If you want to listen for transition ends while many answers bit old you might want to review

How do I normalize CSS3 Transition functions across browsers?

Lynyrd
  • 76
  • 6
  • Thank you for the clarification! I went with adding event listeners. I already found the documentation of Modernizr helpful in other situations, I should check there more often for solutions :D https://modernizr.com/docs/ I'd upvote your reply, but I don't have the 15 reputation I need for upvoting xD – torquan Apr 03 '20 at 08:57
0

In case someone comes across this, here's what I ended up doing (thanks, @Lynyrd!)

// transition names (found in Modernizr)
let transEndEventNames = {
  'WebkitTransition': 'webkitTransitionEnd', // Saf 6, Android Browser
  'MozTransition': 'transitionend', // only for FF < 15
  'transition': 'transitionend', // IE10, Opera, Chrome, FF 15+, Saf 7+
};

I use React, so I added the event listeners in useEffect so that they automatically get added if the foodList is fetched again etc.

// add & remove event listeners for transition end
useEffect(() => {
  foodList.forEach((item, index) => {
    if (!document.getElementById(index)) return;
    document
      .getElementById(index)
      .addEventListener(
        transEndEventNames.WebkitTransition,
        handleTransitionEnd
      );
    document
      .getElementById(index)
      .addEventListener(
        transEndEventNames.MozTransition,
        handleTransitionEnd
      );
    document
      .getElementById(index)
      .addEventListener(transEndEventNames.transition, handleTransitionEnd);
  });

  return () => {
    foodList.forEach((item, index) => {
      if (!document.getElementById(index)) return;
      document
        .getElementById(index)
        .removeEventListener(
          transEndEventNames.WebkitTransition,
          handleTransitionEnd
        );
      document
        .getElementById(index)
        .removeEventListener(
          transEndEventNames.MozTransition,
          handleTransitionEnd
        );
      document
        .getElementById(index)
        .removeEventListener(
          transEndEventNames.transition,
          handleTransitionEnd
        );
    });
  };
}, [foodList]);

I decided against trying to add only one event listener depending on the current browser, but that would be possible too. Keep in mind, that this listener get's triggered for every transition. As a result, my table entries got deleted when somebody hovered over an element in (which triggered a tooltip). So don't forget to filter:

function handleTransitionEnd(event) {
  // only delete element for transform transition
  if (!(event.propertyName === 'transform')) return;
  let newState = foodList.slice();
  newState.splice(event.target.id, 1);
  setFoodList(newState);
}

In my case, this is pretty easy, because I only have one "transform" transition on the element. Also, I have no idea if the amount of eventListeners become a performance problem for larger tables. The setTimeout() function isn't needed at all now, in deleteFood() I now only start the animation and delete the item from my database:

function DeleteFood(index) {
  // animate
  document.getElementById(index).style.transform = 'translate(500px, 0)';

  // delete from airtable
  foodList[index].destroy();
}
torquan
  • 356
  • 1
  • 6