Description
I have a <button>
that, when clicked once, sets the class of a <div>
element to "fade-in" and inserts the <div>
element in the DOM. This visually fades in the <div>
element after insertion into the DOM. When the button is clicked again, the button sets the class of the <div>
element to "fade-out", waits with setTimeout()
, and removes the <div>
element from the DOM. This visually fades out the <div>
element before removal from the DOM.
Problem
If the <div>
element is in the DOM (after clicking once), and I proceed to click twice in rapid succession, then the <div>
element fades out, then fades in, then gets removed from the DOM.
Expected Result
I would like the <div>
element to, from the first click, fade out and be removed from the DOM, then, from the second click, fade in and be reinserted into the DOM. That is, the <div>
element should both 1) be in the DOM and 2) appear visually after rapidly clicking twice when the button has already been pressed once. My assumption is that, because the second click happens before setTimeout()
ends, the container is "inserted" (it already is inserted, though) into the DOM before the code within setTimeout()
is executed (which removes the <div>
from the DOM).
How do I ensure that the code to reinsert the <div>
element (occurring in the second mouse click event) is executed after the setTimeout()
event (occurring in the first mouse click event)?
Code
Attached below is a minimal reproducible example of my problem.
const container = document.createElement("div");
let toggled = false;
const button = document.querySelector("button");
button.addEventListener("click", event => {
toggled = !toggled;
if (toggled) {
container.setAttribute("class", "fade-in");
button.parentElement.insertBefore(container, button.nextSibling);
} else {
container.setAttribute("class", "fade-out");
setTimeout(() => {container.remove();}, 500)
}
})
div {
width: 100px;
height: 100px;
background-color: black;
}
.fade-in {
animation: fade-in 500ms linear;
}
@keyframes fade-in {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
.fade-out {
animation: fade-out 500ms linear;
}
@keyframes fade-out {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="../../CSS/Test.css">
<script src="../../JS/Test.js" async></script>
<title>Document</title>
</head>
<body>
<button> Fade In / Fade Out </button>
</body>
</body>
</html>
I have tried using Promises, but none of my attempts have worked, unfortunately. Because I am new to Javascript, I am not sure whether I am using it wrong, or whether my approach has flaws.
Thanks in advance!
CodePudding user response:
I use a variable inAnimation
that is set to true
when an animation is triggered and set to false
at the end of the animation using setTimeout(f, animationDuration)
:
I also set the animation duration to 1000ms
to see the probleme more clearly.
Only need to change :
const container = document.createElement("div");
let toggled = false;
let inAnimation = false; // is div currently animated
const button = document.querySelector("button");
button.addEventListener("click", event => {
if(inAnimation) return; // test if there is no current animation
if (!toggled) {
inAnimation = true;
container.setAttribute("class", "fade-in");
button.parentElement.insertBefore(container, button.nextSibling);
setTimeout(() => {
inAnimation = false; // end of animation
}, 1000)
} else if(!inAnimation) {
inAnimation = true;
container.setAttribute("class", "fade-out");
setTimeout(() => {
inAnimation = false; // end of animation
container.remove();
}, 1000)
}
toggled = !toggled;
})
div {
width: 100px;
height: 100px;
background-color: black;
}
.fade-in {
animation: fade-in 1000ms linear;
}
@keyframes fade-in {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
.fade-out {
animation: fade-out 1000ms linear;
}
@keyframes fade-out {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="../../CSS/Test.css">
<script src="../../JS/Test.js" async></script>
<title>Document</title>
</head>
<body>
<button> Fade In / Fade Out </button>
</body>
</body>
</html>
CodePudding user response:
Try this solution. If you have a simple animation, it's best to use transition
instead of animation
. It's easy to use and less code. I added a second option (duration) to select the duration time as a setTimeout and inside the CSS.
const container = document.createElement('div');
const button = document.querySelector('button');
let toggled = false;
const duration = 500;
button.addEventListener('click', () => {
toggled = !toggled;
if (toggled) {
button.parentElement.insertBefore(container, button.nextSibling);
const setClass = setTimeout(() => {
// Set the attributes
// Pass duration value to the CSS variable using template string
container.setAttribute('style', `--duration: ${duration}ms`);
// This trigger to smoothly transition to the new property,
// because we are overriding it.
container.setAttribute('class', 'show');
// Clear 'setTimeout'
clearTimeout(setClass);
}, 0);
} else {
// First, remove attribute
// This trigger to smoothly transition to the initial property
container.removeAttribute('class');
// Remove the container and clear 'setTimeout'
const removeElement = setTimeout(() => {
container.remove();
clearTimeout(removeElement);
}, duration);
}
});
div {
width: 100px;
height: 100px;
background-color: black;
opacity: 0; /* new line */
transition: opacity var(--duration); /* new line */
}
/* This is the trigger class */
.show {
opacity: 1;
}
<button>Fade In / Fade Out</button>