javascriptcssanimation

Inconsistent CSS animation using JavaScript in response to a click event listener


I'm working on a routine that reorders a set of div on an HTML page using JavaScript. The re-ordering is working correctly.

To add some visual feedback when the div moves, I'm using a CSS animation (and keyframes) class that gets added

Each div has an up and down arrow with an event listener.

The "Up" functionality works every time. The "Down" functionality works exactly once, unless the Up button is used, in which case the Down functionality is reset and works just once again.

The entire experiment is in a self-contained PHP file:

<?php
    ini_set('display_errors', 1);
?>

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
    <title>Reorder List</title>
    <style>
        .component {
            margin-bottom: 4px;
            font-size: 16px;
            border: 1px solid black;
            border-radius: 5px;
        }

        .component button {
            margin: 2px;
            padding: 2px;
            border-radius: 5px;
            border: 1px solid grey;
            font-size: 24px;
        }

        .component span {
            margin-left: 10px;
        }

        .container {
            border: 1px solid red;
            padding: 5px;
            border-radius: 5px;
        }

        .internal {
            clear: both;
            display: block;
            margin-left: 15px;
        }

        .fade-in {
            animation: fadeIn 1s ease-in-out;
        }

        @keyframes fadeIn {
            from {
                opacity: 0;
            }
            to {
                opacity: 1;
            }
        }

    </style>
</head>
<body>

<p>[<a href="/experiments/">Experiments Home</a>]</p>

<p>This page demonstrates: </p>

<ol>
    <li>Reordering child divs of a parent div using JavaScript.</li>
    <li>The order set by JavaScript is retained in a POST request.</li>
</ol>

<?php

$colors = [
    "red" => "RED",
    "green" => "GREEN",
    "cyan" => "CYAN",
    "darkmagenta" => "DARK MAGENTA",
    "blue" => "BLUE",
    "darksalmon" => "DARK SALMON",
    "lightcoral" => "LIGHT CORAL",
    "mediumspringgreen" => "MEDIUM SPRING GREEN",
    "indigo" => "INDIGO"
];

if ( $_POST ) {
    $colorlist = $_POST;
} else {
    $colorlist = $colors;
} // end else

?>

    <form method="post" action="reorder_list.php">
    <div class="container" id="list">

    <?php
    $i = 0;
    foreach ($colorlist as $key => $value) {
        $i++;
    ?>

        <div class="component">
            <button class="up material-icons">arrow_upward</button>
            <button class="down material-icons">arrow_downward</button>
            <input type="hidden" name="<?= $key ?>" value="<?= $value ?>">
            <p class="internal" style="color: <?= $key ?>"><?= $i . ". " . $value ?></p>
            <p class="internal">Here is another paragraph</p>
            <p class="internal"><label for="name">Here is a form element</label> <input type="text" id="name" name="name"></p>
            <p class="internal">And yet another paragraph</p>
        </div>
    <?php
    } // end foreach  ?>
    </div>

    <p><input type="submit"></p>
    </form>


<script>
    document.querySelectorAll('.up').forEach(button => {
        button.addEventListener('click', function(event) {
            once: true;
            event.preventDefault();
            const el = button.parentElement;
            const prevEl = el.previousElementSibling;
            if (prevEl) {
                el.parentNode.insertBefore(el, prevEl);
                //el.classList.remove('fade-in');
                el.classList.add('fade-in');
                //setTimeout(el.classList.remove('fade-in'), 3000);
            }
        });
    });

    document.querySelectorAll('.down').forEach(button => {
        button.addEventListener('click', function(event) {
            once: true;
            event.preventDefault();
            const el = button.parentElement;
            const nextEl = el.nextElementSibling;
            if (nextEl) {
                el.parentNode.insertBefore(nextEl, el);
                //el.classList.remove('fade-in');
                el.classList.add('fade-in');
                //setTimeout(el.classList.remove('fade-in'), 3000);
            }
        });
    });
</script>


<h1>Original Order</h1>
<ul id="list">
<?php
$i = 0;
foreach ($colors as $key => $value) {
    $i++;
?>
    <li class="list-item"><span style="color: <?= $key; ?>"><?= $i . ". " . $value ?></span></li>
<?php
}
?>

</ul>

</body>
</html>

I have tried using setTimeOut to remove the animation class which disables the Up and Down functionally completely. (You can see the attempt commented out in the code)

I also tried removing the animation class before adding it back in.

I tried adding once: true to the event

The goal is to get both Up and Down functionality working consistently.


Solution

  • I created a minimal reproducible example from your code and it successfully does the up and down movement even repeatedly. However, the animation works only and exactly once either way. If you do n moves downwards, only the first animates, until you do m move upwards, only the first animates, then the next down animates, etc.:

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
        <title>Reorder List</title>
        <style>
            .component {
                margin-bottom: 4px;
                font-size: 16px;
                border: 1px solid black;
                border-radius: 5px;
            }
    
            .component button {
                margin: 2px;
                padding: 2px;
                border-radius: 5px;
                border: 1px solid grey;
                font-size: 24px;
            }
    
            .component span {
                margin-left: 10px;
            }
    
            .container {
                border: 1px solid red;
                padding: 5px;
                border-radius: 5px;
            }
    
            .internal {
                clear: both;
                display: block;
                margin-left: 15px;
            }
    
            .fade-in {
                animation: fadeIn 1s ease-in-out;
            }
    
            @keyframes fadeIn {
                from {
                    opacity: 0;
                }
                to {
                    opacity: 1;
                }
            }
    
        </style>
    </head>
    <body>
    
    <p>[<a href="/experiments/">Experiments Home</a>]</p>
    
    <p>This page demonstrates: </p>
    
    <ol>
        <li>Reordering child divs of a parent div using JavaScript.</li>
        <li>The order set by JavaScript is retained in a POST request.</li>
    </ol>
    
    <script>
    
    let colors = {
        red: "RED",
        green: "GREEN",
        cyan: "CYAN",
        darkmagenta: "DARK MAGENTA",
        blue: "BLUE",
        darksalmon: "DARK SALMON",
        lightcoral: "LIGHT CORAL",
        mediumspringgreen: "MEDIUM SPRING GREEN",
        indigo: "INDIGO"
    };
    
    let colorlist = colors;
    
    </script>
    <div id="components">
    </div>
    
        <form method="post" action="reorder_list.php">
        <div class="container" id="list">
    
    <script>
        let i = 0;
        let template = "";
        for (let key in colorlist) {
            i++;
            let value = colorlist[key];
            template += `
            <div class="component">
                <button class="up material-icons">arrow_upward</button>
                <button class="down material-icons">arrow_downward</button>
                <input type="hidden" name="${key}" value="${value}">
                <p class="internal" style="color: ${key}">${i + ". " + value }</p>
                <p class="internal">Here is another paragraph</p>
                <p class="internal"><label for="name">Here is a form element</label> <input type="text" id="name" name="name"></p>
                <p class="internal">And yet another paragraph</p>
            </div>
            `
        }
        document.getElementById("components").innerHTML = template;
    </script>
        </div>
    
        <p><input type="submit"></p>
        </form>
    
    
    <script>
        document.querySelectorAll('.up').forEach(button => {
            button.addEventListener('click', function(event) {
                once: true;
                event.preventDefault();
                const el = button.parentElement;
                const prevEl = el.previousElementSibling;
                if (prevEl) {
                    el.parentNode.insertBefore(el, prevEl);
                    //el.classList.remove('fade-in');
                    el.classList.add('fade-in');
                    //setTimeout(el.classList.remove('fade-in'), 3000);
                }
            });
        });
    
        document.querySelectorAll('.down').forEach(button => {
            button.addEventListener('click', function(event) {
                once: true;
                event.preventDefault();
                const el = button.parentElement;
                const nextEl = el.nextElementSibling;
                if (nextEl) {
                    el.parentNode.insertBefore(nextEl, el);
                    //el.classList.remove('fade-in');
                    el.classList.add('fade-in');
                    //setTimeout(el.classList.remove('fade-in'), 3000);
                }
            });
        });
    </script>
    
    
    <h1>Original Order</h1>
    <ul id="list">
    </ul>
    <script>
    i = 0;
    template = "";
    for (let key in colors) {
        i++;
        let value = colors[key];
        template += `
            <li class="list-item"><span style="color: ${key}">${i + ". " + value }</span></li>
        `;
    }
    document.getElementById("list").innerHTML = template;
    
    </script>
    
    
    </body>
    </html>

    I presume that this animation issue is the issue you intend to fix. Therefore:

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
        <title>Reorder List</title>
        <style>
            .component {
                margin-bottom: 4px;
                font-size: 16px;
                border: 1px solid black;
                border-radius: 5px;
            }
    
            .component button {
                margin: 2px;
                padding: 2px;
                border-radius: 5px;
                border: 1px solid grey;
                font-size: 24px;
            }
    
            .component span {
                margin-left: 10px;
            }
    
            .container {
                border: 1px solid red;
                padding: 5px;
                border-radius: 5px;
            }
    
            .internal {
                clear: both;
                display: block;
                margin-left: 15px;
            }
    
            .fade-in {
                animation: fadeIn 1s ease-in-out;
            }
    
            @keyframes fadeIn {
                from {
                    opacity: 0;
                }
                to {
                    opacity: 1;
                }
            }
    
        </style>
    </head>
    <body>
    
    <p>[<a href="/experiments/">Experiments Home</a>]</p>
    
    <p>This page demonstrates: </p>
    
    <ol>
        <li>Reordering child divs of a parent div using JavaScript.</li>
        <li>The order set by JavaScript is retained in a POST request.</li>
    </ol>
    
    <script>
    
    let colors = {
        red: "RED",
        green: "GREEN",
        cyan: "CYAN",
        darkmagenta: "DARK MAGENTA",
        blue: "BLUE",
        darksalmon: "DARK SALMON",
        lightcoral: "LIGHT CORAL",
        mediumspringgreen: "MEDIUM SPRING GREEN",
        indigo: "INDIGO"
    };
    
    let colorlist = colors;
    
    </script>
    <div id="components">
    </div>
    
        <form method="post" action="reorder_list.php">
        <div class="container" id="list">
    
    <script>
        let i = 0;
        let template = "";
        for (let key in colorlist) {
            i++;
            let value = colorlist[key];
            template += `
            <div class="component" data-color="${value}">
                <button class="up material-icons">arrow_upward</button>
                <button class="down material-icons">arrow_downward</button>
                <input type="hidden" name="${key}" value="${value}">
                <p class="internal" style="color: ${key}">${i + ". " + value }</p>
                <p class="internal">Here is another paragraph</p>
                <p class="internal"><label for="name">Here is a form element</label> <input type="text" id="name" name="name"></p>
                <p class="internal">And yet another paragraph</p>
            </div>
            `
        }
        document.getElementById("components").innerHTML = template;
    </script>
        </div>
    
        <p><input type="submit"></p>
        </form>
    
    
    <script>
        document.querySelectorAll('.up').forEach(button => {
            button.addEventListener('click', function(event) {
                once: true;
                event.preventDefault();
                const el = button.parentElement;
                const prevEl = el.previousElementSibling;
                if (prevEl) {
                    let pn = el.parentNode;
                    el.parentNode.insertBefore(el, prevEl);
                    let newNode = pn.querySelector(".component[data-color='" + el.getAttribute("data-color") + "']");
                    //el.classList.remove('fade-in');
                    setTimeout(function() {newNode.classList.add('fade-in')}, 0);
                    setTimeout(newNode.classList.remove('fade-in'), 1000);
                }
            });
        });
    
        document.querySelectorAll('.down').forEach(button => {
            button.addEventListener('click', function(event) {
                once: true;
                event.preventDefault();
                const el = button.parentElement;
                const nextEl = el.nextElementSibling;
                if (nextEl) {
                    let pn = el.parentNode;
                    el.parentNode.insertBefore(nextEl, el);
                    let newNode = pn.querySelector(".component[data-color='" + el.getAttribute("data-color") + "']");
                    //el.classList.remove('fade-in');
                    el.classList.add('fade-in');
                    setTimeout(function() {newNode.classList.add('fade-in')}, 0);
                    setTimeout(newNode.classList.remove('fade-in'), 1000);
                }
            });
        });
    </script>
    
    
    <h1>Original Order</h1>
    <ul id="list">
    </ul>
    <script>
    i = 0;
    template = "";
    for (let key in colors) {
        i++;
        let value = colors[key];
        template += `
            <li class="list-item"><span style="color: ${key}">${i + ". " + value }</span></li>
        `;
    }
    document.getElementById("list").innerHTML = template;
    
    </script>
    
    
    </body>
    </html>

    What the problem was? You move HTML elements, that is, you destroy and recreate them. Which means that by the time your new element, the copy is created the second time, it will already have the fade in animation without the animation itself. This is why instead I am searching for the new node instead and add the class to it and then remove the class from it.