Follow

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use
Contact

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.

MEDevel.com: Open-source for Healthcare and Education

Collecting and validating open-source software for healthcare, education, enterprise, development, medical imaging, medical records, and digital pathology.

Visit Medevel

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,GRAD@20..48,100..700,0..1,-50..200"
    />

    <!-- <script src="https://cdn.tailwindcss.com"></script> -->
    <script
      defer
      src="https://cdn.jsdelivr.net/npm/@alpinejs/persist@3.x.x/dist/cdn.min.js"
    ></script>
    <script
      defer
      src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/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.

>Solution :

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.

Add a comment

Leave a Reply

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use

Discover more from Dev solutions

Subscribe now to keep reading and get access to the full archive.

Continue reading