4

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.

1 Answer 1

1

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.

Sign up to request clarification or add additional context in comments.

2 Comments

I love you stranger! Would you mind expanding on how using the index as the key was causing that to happen?
With index as the key, Alpine sees something like [0, 1, 2] for the keys for [item0, item1, item2]. If we remove item1, then the keys we pass for the data is [0, 1]. Notice how there is no 2. Alpine does not look at the full array, only the keys. It sees 2 is no longer there, so it removes that item, which as far as it is aware, corresponds to item2 so it only renders [item0, item1].

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.