Вопрос или проблема
Я пытаюсь создать что-то вроде списка задач. Структура приложения проста – у меня есть одно состояние для хранения данных задач и 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) с возможностью редактирования элементов. Давайте разберём основные аспекты вашей реализации и способы их улучшения.
Описание проблем
-
Добавление задач в центре списка: При добавлении задачи между существующими элементами, оставшиеся элементы списка "обрезаются", и новый элемент появляется в конце списка.
-
Проблемы с сохранением текста при клике: Когда вы добавляете задачу в случайное место списка и кликаете на задачу перед новой, текст исчезает.
Эти проблемы могут быть связаны с неправильным управлением состоянием и 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. Это упростит логику вашего компонента и устранит существующие проблемы при добавлении и редактировании задач.