Animate the removal or addition of an item in a list

phuocng

Phuoc Nguyen

Posted on December 28, 2023

Animate the removal or addition of an item in a list

Working with dynamic items in a list is a common task in web development, especially when dealing with user-generated content. For example, think about a social media platform where users can post comments on a particular post. The comments section is constantly changing as users add or delete their comments. Another example is an e-commerce website that displays a list of products on the page. When a user adds or removes items from their cart, the list of products displayed changes dynamically.

To enhance the user experience, we can automatically animate the addition or removal of an item in the list. This provides visual feedback and gives our web applications a more polished and professional look.

In this post, we'll show you how to achieve this effect using a MutationObserver. By watching for changes to the list, we can trigger an animation whenever an item is added or removed.

To make things easier, we'll use a simple todo list application as an example. Users can add new tasks or remove existing ones, and we'll demonstrate how the animation works in this context.

Building a simple task management app

Let's dive into building a basic todo app! First, we need to organize the markup, which consists of two elements: an input field for adding new tasks and a display area for the list of tasks.

Here's how it's organized:

<div class="todo">
    <input class="todo__input" placeholder="Enter new task ..." type="text" id="newTask" />
    <div id="tasks"></div>
</div>
Enter fullscreen mode Exit fullscreen mode

To add a new task, users can simply type it into the main input and hit Enter. To make this happen, we handle the input event which is triggered whenever the user modifies the input's value.

In the handler, we check if the input is not empty. We also determine if the user has pressed the Enter key by checking the key property.

const newTaskInput = document.getElementById('newTask');

newTaskInput.addEventListener('keydown', (e) => {
    const newTask = e.target.value;
    if (newTask && e.key === 'Enter') {
        // Add new task ...
    }
});
Enter fullscreen mode Exit fullscreen mode

Let's keep things simple and skip the unnecessary details for this post. We won't bother with using variables to store the task model. Instead, we'll create a new element to display the newly created task. This element will have two components: one for displaying the task and one for removing it from the list. Here's an example:

// Add new task
const div = document.createElement("div");
div.classList.add("todo__task");

const taskEle = document.createElement("div");
taskEle.classList.add("todo__label");
taskEle.innerHTML = newTask;

const removeBtn = document.createElement("button");
removeBtn.innerHTML = "Remove";
removeBtn.classList.add("todo__button");
Enter fullscreen mode Exit fullscreen mode

In this example, we create a new div element to represent a task. Within that div, we create two elements: taskEle for displaying the task and removeBtn for removing the task. When a user clicks the remove button, we remove the entire task element.

removeBtn.addEventListener("click", () => {
    div.remove();
});
Enter fullscreen mode Exit fullscreen mode

Next, we simply use the append function to add these two elements to the task element. Then, we append the task element to the list of tasks element.

div.append(taskEle, removeBtn);
tasksEle.appendChild(div);
Enter fullscreen mode Exit fullscreen mode

Up to this point, we've used straightforward DOM manipulation functions to create new task elements or remove them from the list as users add or remove tasks.

This is all pretty basic stuff, with no fancy animations involved.

Adding animation to new items

Let's talk about how to make our todo app even better. We've already got the basics covered with creating and removing tasks, but let's take it up a notch and add some automatic animation to new tasks.

To do this, we'll use a powerful API called MutationObserver. This API lets us track changes to the DOM, like when a new task is added, and then take action in response to those changes. It's a really handy tool for making our web app more dynamic and engaging.

When we create a MutationObserver instance, we pass in a callback function that gets called whenever a change is detected. This function gets an array of MutationRecord objects as its first argument, which tells us what changed in the DOM.

We also pass in an options object that specifies what changes we want to observe. For our todo app, we only care about additions or removals of child nodes from the tasks element. So we set the childList property to true, and the other properties to false.

By using MutationObserver, we can easily detect and respond to changes in our app's UI without having to constantly check for updates.

const tasksEle = document.getElementById('tasks');

const mo = new MutationObserver(mutationCallback);
mo.observe(tasksEle, {
    attributes: false,
    characterData: false,
    childList: true,
    subtree: false,
});
Enter fullscreen mode Exit fullscreen mode

This code creates a MutationObserver instance and passes it a callback function to monitor changes to a specific element (tasksEle). The observe() method is then called with the element and an options object to specify the types of mutations we want to observe (childList).

The mutationCallback function is triggered by the MutationObserver instance whenever a change is detected in the observed element. It receives two arguments: an array of MutationRecord objects that describe the changes that occurred, and a reference to the MutationObserver instance.

In our case, we only want to track changes to child nodes being added to the tasks element. To do this, we iterate over the addedNodes property of each MutationRecord, which contains a list of all the nodes that were added to the observed element as direct children.

Here's how we implement the callback:

const mutationCallback = (entries, instance) => {
    entries.forEach((entry) => {
        entry.addedNodes.forEach((newNode) => animateNewNode(newNode));
    });
};
Enter fullscreen mode Exit fullscreen mode

To bring each new node into view, we use a separate function called animateNewNode(). This function is responsible for handling the animation process.

const animateNewNode = (ele) => {
    ele.style.overflow = "hidden";
    ele.animate([
        {
            height: "0px",
            opacity: 0,
        },
        {
            height: `${ele.scrollHeight}px`,
            opcity: 1,
        },
    ], {
        duration: 400,
    })
    .addEventListener("finish", () => {
        ele.style.overflow = "";
    });
};
Enter fullscreen mode Exit fullscreen mode

The animateNewNode() function is in charge of animating the newly added task element, making it look smooth and visually pleasing. First, it sets the overflow property to hidden to prevent any content from overflowing during the animation.

Next, it uses the animate() method to create a new animation. The animation transitions the element's height and opacity properties over a duration of 400 milliseconds. At first, the element is invisible with a height of 0 pixels and an opacity of 0. But as the animation progresses, the element becomes visible with a height equal to the element's scroll height and an opacity of 1.

Finally, the function adds an event listener for the finish event on the animation object. This listener removes the overflow property from the element so that its content can overflow normally after the animation has finished.

By calling this function for each new task added to our todo list, we can provide a smooth transition as tasks are added to the list.

Try it out by entering a new task in the main input and pressing Enter. You'll see the new task slide in from the top to bottom automatically.

Animating the removal of an item

We'll take a similar approach to the previous section to animate the removal of items. In addition to added nodes, MutationObserver also provides access to removed nodes through the removedNodes property.

const mutationCallback = (entries, instance) => {
    entries.forEach((entry) => {
        entry.removedNodes.forEach((removedNode) => {
            animateRemovedNode(removedNode, entry.previousSibling, entry.nextSibling);
        });
    });
};
Enter fullscreen mode Exit fullscreen mode

If a node has been removed from a page, you may wonder how it can still be animated. The answer is simple: we can create a clone of the node using its instance, and insert it at the desired position. In this case, the MutationRecord provides the previousSibling and nextSibling properties, which are the previous and next siblings of the removed node.

Within the animateRemovedNode function, we start by creating a clone of the removed node with its cloneNode() method. This creates a fresh copy of the node that we can then insert back into the DOM.

const animateRemovedNode = (ele, previousSibling, nextSibling) => {
    // Create a clone of the removed node
    const clonedNode = ele.cloneNode(true);

    // Insert the cloned node at the appropriate position
    if (nextSibling && nextSibling.parentNode) {
        nextSibling.parentNode.insertBefore(clonedNode, nextSibling);
    } else if (previousSibling && previousSibling.parentNode) {
        previousSibling.parentNode.appendChild(clonedNode);
    }
};
Enter fullscreen mode Exit fullscreen mode

Now that we have our cloned node, we can use it to create our animation just like we did before. By inserting this cloned node back into the DOM, it appears to users that the original item is being animated out of view.

Check out this sample code for an idea of how we animate the cloned node:

clonedNode.style.transformOrigin = "center";
clonedNode.style.overflow = "hidden";
clonedNode.animate([
    {
        height: `${clonedNode.scrollHeight}px`,
        opcity: 1,
    },
    {
        height: "0px",
        opacity: 0,
    },
], {
    duration: 400,
    easing: "linear",
}).addEventListener("finish", () => {
    clonedNode.style.overflow = "";
    clonedNode.remove();
});
Enter fullscreen mode Exit fullscreen mode

To animate the clonedNode, we take a few steps. First, we set its transformOrigin property to center to ensure that the animation scales from the center of the element. We also set its overflow property to hidden so that its content doesn't overflow during the animation.

Next, we use the animate() method to create a new animation that transitions the element's height and opacity properties over a duration of 400 milliseconds. The animation starts with a height equal to the clone's scroll height and an opacity of 1 (i.e., fully visible) and ends with a height of 0 pixels and an opacity of 0 (i.e., invisible).

Finally, we add an event listener for the finish event on the animation object. This event is triggered when the animation has completed. In the event listener, we remove the overflow property from the element so that its content can overflow normally after the animation has finished, and then remove it from the DOM.

Differentiating the cloned node with added task elements

When we clone a removed node and add it back to the task element, it will be treated as a new node and trigger the animateNewNode() function we created earlier. However, to avoid the cloned node from triggering the animateNewNode() function again, we can add a custom attribute to it.

In this case, we'll call the attribute DYNAMIC_NODE_ATTR. Before animating a new node, we first check if it has this attribute. If it does, then we know that it's a cloned node and should not be animated again.

Here's how we can use this attribute:

const DYNAMIC_NODE_ATTR = 'data-dynamic-node';

const animateNewNode = (ele) => {
    if (ele.hasAttribute(DYNAMIC_NODE_ATTR)) {
        return;
    }
    // Continue animating the new added item ...
};

const animateRemovedNode = (ele, previousSibling, nextSibling) => {
    ele.setAttribute(DYNAMIC_NODE_ATTR, 'true');
};
Enter fullscreen mode Exit fullscreen mode

Now, whenever we add or remove a task from our todo list app, the animation runs smoothly without any issues. But, when we remove a task, we need to make sure that we don't trigger the animateRemovedNode() function again for the cloned node.

To solve this issue, we can use a similar approach to what we did with the animateNewNode() function. We add a custom attribute to the cloned node called BEING_REMOVED_ATTR.

Before animating a removed node, we check if it has this attribute. If it does, we know that it's a cloned node and should not be animated again. This is how we use the BEING_REMOVED_ATTR attribute.

const BEING_REMOVED_ATTR = 'data-being-removed';

const animateRemovedNode = (ele, previousSibling, nextSibling) => {
    const clonedEle = ele.cloneNode(true);
    if (clonedEle.hasAttribute(BEING_REMOVED_ATTR)) {
        return;
    }

    clonedEle.setAttribute(BEING_REMOVED_ATTR, 'true');
    // Continue animating the cloned item ...
}
Enter fullscreen mode Exit fullscreen mode

By checking for this attribute before animating a removed node, we ensure that only actual nodes being removed from our todo list are animated out of view.

Tip

By using a custom attribute, we can solve our problems without having to use a custom data structure or additional variables to store added or removed items. This technique is incredibly useful and can be applied in many different situations.

Let's check out the final result in the playground. You have the freedom to adjust the animations we created inside the animateNewNode and animateRemovedNode functions to suit your preferences.

With this new addition to our code, we now have a fully functional todo app with smooth and visually appealing animations for adding and removing tasks.

Conclusion

As we've seen, adding animations to our todo list app can greatly enhance the user experience. By animating the addition and removal of tasks, we give users a better sense of what's happening on the page and help them stay focused on their goals.

In this post, we learned how to use MutationObserver to detect changes to our todo list and create animations for added and removed tasks. We also explored how to differentiate between newly added nodes and cloned nodes so that we can avoid running unnecessary animations.

With these techniques in mind, you can add more advanced animations to your own web applications. But remember, animations should always be used in moderation and with purpose, as they can quickly become distracting or overwhelming if overused.


If you want more helpful content like this, feel free to follow me:

💖 💪 🙅 🚩
phuocng
Phuoc Nguyen

Posted on December 28, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related