Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Alpine.js Not rendering properly after deleting from an array of objects

I am making a to-do/task list app. I have written a function that is meant to delete an object from the taskList array. async deleteTask(taskList, index){taskList = await taskList.splice(index,1)} I can see in local storage that it deletes the proper object from the array, however in the render <template x-for="(task, index) in filteredTasks" :key="index"> it deletes the final entry on the list, rather than the one I've tried to delete (despite the proper one being deleted in localStorage).

It is rendering based on filteredTasks rather than taskList - but I've told filteredTasks to update after the delete function is run. <button aria-label="Delete Task" x-on:click=" await deleteTask(taskList, index); filteredTasks = [...taskList]">

If I refresh the page, or add a new task to the list, the render corrects itself.

Here is my overall code:

<!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" />
    <title>Task List</title>
    <link
      rel="stylesheet"
      href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,[email protected],100..700,0..1,-50..200"
    />

    <!-- <script src="https://cdn.tailwindcss.com"></script> -->
    <script
      defer
      src="https://cdn.jsdelivr.net/npm/@alpinejs/[email protected]/dist/cdn.min.js"
    ></script>
    <script
      defer
      src="https://cdn.jsdelivr.net/npm/[email protected]/dist/cdn.min.js"
    ></script>

    <script>
      function methods() {
        return {
          async pushTask(taskList) {
            if (document.getElementById("taskTitleInput").value === "") {
              document.getElementById("taskTitleInput").value = "No Title";
            }
            if (document.getElementById("taskDescriptionInput").value === "") {
              document.getElementById("taskDescriptionInput").value =
                "No Description";
            }
            taskList.push({
              title: document.getElementById("taskTitleInput").value,
              description: document.getElementById("taskDescriptionInput")
                .value,
              completed: false,
            });
            document.getElementById("taskTitleInput").value = "";
            document.getElementById("taskDescriptionInput").value = "";
          },
          async firstTask(taskList) {
            if (taskList.length === 0) {
              taskList.push({
                title: "Add a Task",
                description: "Add your first task to get started!",
                completed: false,
              });
            }
          },
          async deleteTask(taskList, index){
            taskList = await taskList.splice(index,1)
          }
        };
      }
    </script>
    <style>
      [x-cloak] {
        display: none;
      }
    </style>
  </head>
  <body x-data="methods()">
    <h1>Personal Task Manager</h1>
    <div
      aria-label="Task Manager App"
      x-data="{ taskList: $persist([]), filteredTasks: [] }"
      x-directives="{persist: $persist}"
    >
      <form aria-label="New Task Form">
        <label for="taskTitle">Task title:</label
        ><input type="text" id="taskTitleInput" name="taskTitle" value="" />
        <label for="taskDescription">Task description:</label
        ><input
          type="text"
          id="taskDescriptionInput"
          name="taskDescription"
          value=""
        />
        <button
          x-on:click="await pushTask(taskList); $nextTick(()=>{filteredTasks = [...taskList]})"
        >
          Add Task
        </button>
      </form>
      <div
        aria-label="Task List"
        x-init="$nextTick(()=>{filteredTasks = [... taskList]})"
      >
        <div aria-label="Filters">
          <button aria-label="All" x-on:click="filteredTasks = [...taskList]">
            All</button
          ><button
            aria-label="Incomplete"
            x-on:click="filteredTasks = taskList.filter(task => task.completed === false
        )"
          >
            Incomplete</button
          ><button
            aria-label="Complete"
            x-on:click="filteredTasks = taskList.filter(task => task.completed === true)"
          >
            Complete
          </button>
        </div>
        <!-- Another div with additional features here -->
        <div aria-label="Tasks" x-init="firstTask(taskList)">
          <template x-for="(task, index) in filteredTasks" :key="index">
            <div x-data="{open: false}">
              <div x-show="!open">
                <input
                  x-on:click="task.completed = !task.completed"
                  type="checkbox"
                  x-bind:id="`checkbox-${index}`"
                  x-bind:value="task.completed"
                />
                <h3 x-text="task.title"></h3>
                <p x-text="task.description"></p>
              </div>
              <form x-show="open">
                <label for="editTitle">Edit title:</label
                ><input
                  type="text"
                  x-bind:id="`editTitleInput-${index}`"
                  name="editTitle"
                  x-model:placeholder="task.title"
                />
                <label for="editDescription">Edit description:</label
                ><input
                  type="text"
                  x-bind:id="`editDescriptionInput-${index}`"
                  name="editDescription"
                  x-model="task.description"
                />
                <button
                  x-on:click="open = false; $nextTick(()=>{filteredTasks = [...taskList]})"
                >
                  Submit Changes
                </button>
              </form>

              <button x-on:click="open = ! open" aria-label="Edit Task">
                <span class="material-symbols-outlined"> edit </span>
              </button>
              <button
                aria-label="Delete Task"
                x-on:click=" await deleteTask(taskList, index);
                filteredTasks = [...taskList]"
              >
                <span class="material-symbols-outlined"> close </span>
              </button>
            </div>
          </template>
        </div>
      </div>
    </div>
  </body>
</html>

I have tried:

  • Having the function in line rather than as a method - it did the same thing. I changed it to a method specifcally so I can try and explicitly define it as an async function that needs to be awaited, but it didn't help.

  • Making the x-for render based on taskList rather than filteredTasks - it does the same exact thing.

  • Making the function filter out the specific task rather than using splice. It has the exact same rendering problem.

like image 386
alemulli Avatar asked Dec 05 '25 18:12

alemulli


1 Answers

It is because you are using the index as the value for :key and this is what Alpine.js uses to track which element to remove. Consider having some sort of unique ID per task and using that as the :key instead:

taskList.push({
  title: document.getElementById("taskTitleInput").value,
  description: document.getElementById("taskDescriptionInput")
    .value,
  id: Date.now(),
  completed: false,
});

// …

taskList.push({
  title: "Add a Task",
  description: "Add your first task to get started!",
  id: Date.now(),
  completed: false,
});
<template x-for="(task, index) in filteredTasks" :key="task.id">

See this JSFiddle for a full example.

like image 187
Wongjn Avatar answered Dec 08 '25 07:12

Wongjn



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!