Не удается добавить корректный редактируемый элемент li в DOM.

Вопрос или проблема

Я пытаюсь создать что-то вроде списка задач. Структура приложения проста – у меня есть одно состояние для хранения данных задач и useEffect, который добавляет обработчики событий для каждой задачи. Задачу можно добавить двумя способами – из основного поля ввода и из дополнительного поля ввода (которое появляется после нажатия клавиши Enter в строке задачи).

Первая часть работает идеально – все задачи, добавленные через основное поле ввода, отображаются в списке и все они могут быть отредактированы по клику, и добавляется дополнительное поле ввода для добавления новой задачи.

Во второй части у меня есть проблемы:

  • когда я добавляю задачу в конце списка, моё приложение работает идеально, но когда я добавляю задачу в центре списка, финальный список обрезается (например, у меня есть 5 элементов, добавляю новую задачу после 3-го элемента, и 4-5 элементы удаляются), и в конце списка снова появляется новая задача
  • когда я добавляю задачу в случайное место списка и кликаю на задачу перед новой задачей, значение этой задачи исчезает

У меня нет идеи, как это решить…

function ChecklistEditor() {
  const [list, setList] = useState([]);
  const [inputValue, setInputValue] = useState("");

  const add = (e) => {
    e.preventDefault();
    if (
      inputValue.length > 0 &&
      !Array.prototype.find.call(list, (item) => item.taskText === inputValue)
    ) {
      const id = `task${Math.random() * 10000000000000000}`;
      setList((prevList) => [
        ...prevList,
        { id, taskText: inputValue, inProgress: true, done: false, withListner: false },
      ]);
      setInputValue("");
    }
  };

  const setTaskStatus = (id, status) => {
    const changeRecord = list.findIndex(
      (item) => item.id === id.split("-").splice(0, 1).toString(),
    );
    setList((prevList) =>
      prevList.toSpliced(changeRecord, 1, {
        ...prevList[changeRecord],
        done: status,
        inProgress: !status,
      }),
    );
  };

  useEffect(() => {}, [list]);

  useEffect(() => {
    list.forEach((item) => {
      if (item.withListner !== true) {
        const index = list.findIndex((it) => it.id === item.id);
        setList((prevList) =>
          prevList.toSpliced(index, 1, { ...prevList[index], withListner: true }),
        );

        const liItem = document.getElementById(item.id);
        const liText = liItem.querySelector(".checkbox__title");
        liText.addEventListener("click", function changeTask(e) {
          e.preventDefault();
          const liInput = document.createElement("input");
          liInput.setAttribute("type", "text");
          liInput.value = liText.innerText;

          liText.innerText = "";

          liText.appendChild(liInput);
          liInput.focus();
          liText.removeEventListener("click", changeTask);

          function updateTaskText() {
            if (liText.innerText !== liInput.value) {
              liText.innerText = liInput.value;
              // liInput.value = "";

              const changeRecordIndex = list.findIndex((recordItem) => recordItem.id === item.id);
              if (list[changeRecordIndex].taskText !== liItem.innerText) {
                setList((prevList) =>
                  prevList.toSpliced(changeRecordIndex, 1, {
                    ...prevList[changeRecordIndex],
                    taskText: liText.innerText,
                  }),
                );
              }
            }
            liInput.remove();
            liInput.removeEventListener("blur", updateTaskText);
            liText.addEventListener("click", changeTask);
          }

          liInput.addEventListener("blur", updateTaskText);

          liInput.addEventListener("keyup", (event) => {
            const liNewInput = document.createElement("input");
            if (event.code === "Enter") {
              event.preventDefault();
              const liNew = document.createElement("li");
              liNew.setAttribute("id", "temp");

              liItem.after(liNew);
              liNew.appendChild(liNewInput);
              liNewInput.focus();

              liInput.removeEventListener("keyup", () => {});

              liNewInput.addEventListener("blur", () => {
                const newTask = {
                  id: "0000",
                  taskText: liNewInput.value,
                  inProgress: true,
                  done: false,
                  withListner: false,
                };

                const newList = [...list];

                newList.splice(index + 1, 0, newTask);

                setList(newList);

                liNew.removeChild(liNewInput);
                const delTempLi = document.getElementById("temp");
                delTempLi.remove();
                liNewInput.value = "";

                liNewInput.removeEventListener("blur", () => {});
              });
            }
          });
        });
      }
    });
  }, [list]);

  // console.log(list);

  return (
    <form className="task-list" onSubmit={(e) => add(e)}>
      <ul className="task-list__list">
        {list.map((item) => (
          <li id={item.id} key={item.id} className="task-list__item">
            <CheckBox
              id={`${item.id}-checkBox`}
              title={item.taskText}
              onChange={setTaskStatus}
              extClassNameInput={item.done ? "task-list__checkbox_done" : ""}
              extClassNameContainer="task-list__task-container"
              extClassNameTitle={`task-list__text ${item.done ? "task-list__text_done" : ""}`}
            />
          </li>
        ))}
      </ul>
      <Input
        isLabel={false}
        textLabel
        value={inputValue}
        onChange={(e) => setInputValue(e.target.value)}
        valueCheck={false}
        placeholder="Что хотите добавить в чек-лист?"
        outline={false}
        height
        extClassName="task-list__input"
        containerStyle="task-list__input-container"
        hideErrorField
        button={<IconCheckbox />}
      />
    </form>
  );
}

Мой первый инстинкт говорит, что вы перезаписываете свое состояние с помощью splice в setTaskStatus. Можете попробовать, сработает ли это для вас?

const setTaskStatus = (id, status) => {
  const changeRecord = list.findIndex((item) => item.id === id);
  setList((prevList) =>
    prevList.map((item, index) =>
      index === changeRecord
        ? { ...item, done: status, inProgress: !status }
        : item
    )
  );
};

Ответ или решение

Проблема с редактированием элементов списка в DOM

Вы столкнулись с несколькими проблемами при попытке создать список задач (to-do list) с возможностью редактирования элементов. Давайте разберём основные аспекты вашей реализации и способы их улучшения.

Описание проблем

  1. Добавление задач в центре списка: При добавлении задачи между существующими элементами, оставшиеся элементы списка "обрезаются", и новый элемент появляется в конце списка.

  2. Проблемы с сохранением текста при клике: Когда вы добавляете задачу в случайное место списка и кликаете на задачу перед новой, текст исчезает.

Эти проблемы могут быть связаны с неправильным управлением состоянием и DOM-элементами в вашем компоненте.

Решения

1. Управление состоянием

Вместо использования метода toSpliced, который не всегда корректно обновляет состояние, попробуйте заменить его на более предсказуемый метод map. Например, функция, отвечающая за изменение статуса задачи (setTaskStatus), может быть переписана следующим образом:

const setTaskStatus = (id, status) => {
  setList((prevList) =>
    prevList.map((item) =>
      item.id === id ? { ...item, done: status, inProgress: !status } : item
    )
  );
};

Этот подход гарантирует, что все элементы списка будут учтены, а не обрезаны.

2. Корректное редактирование элементов

Для редактирования текста задачи можно использовать setState на объекте, чтобы избежать ошибок при доступе к DOM. Вместо создания новых элементов li в функции keyup, вы можете управлять состоянием элемента по его индексу. Вот пример, как можно это реализовать:

const handleEdit = (id) => {
  const liItem = document.getElementById(id);
  const liText = liItem.querySelector(".checkbox__title");

  const liInput = document.createElement("input");
  liInput.setAttribute("type", "text");
  liInput.value = liText.innerText;
  liText.innerHTML = ''; // Очистка текста

  liText.appendChild(liInput); // Вставка поля ввода
  liInput.focus();

  liInput.addEventListener("blur", () => {
    setList((prevList) => 
      prevList.map((item) => 
        item.id === id ? { ...item, taskText: liInput.value } : item
      )
    );
    liText.innerText = liInput.value; // Возвращение текста
  });
};

Этот подход уменьшит количество изменений в DOM и добавит ясности при редактировании.

3. Использование useEffect

Ваша функция useEffect, ответственной за установку слушателей событий на элементы списка, требует улучшений. Вместо того чтобы назначать слухителей событий в useEffect, вы можете назначить их непосредственно в компоненте при создании элементов списка. Это может помочь в избежании конфликтов между старыми и новыми слушателями из-за изменений в состоянии.

Заключение

Ваша реализация имеет хороший базовый шаблон, но требует улучшений в управлении состоянием и взаимодействии с DOM. Установите слушатели событий непосредственно во время рендеринга и минимизируйте манипуляции с DOM, управляя состоянием с помощью React. Это упростит логику вашего компонента и устранит существующие проблемы при добавлении и редактировании задач.

Оцените материал
Добавить комментарий

Капча загружается...