Создание ToDo-листа бесконечной вложенности на React, TypeScript и MobX
Рассказываем, как создать Todo-list с бесконечной вложенностью подзадач. Для создания приложения используются React, TypeScript и Mobx.
4К открытий8К показов
Всем доброго времени суток! В данной статье я расскажу о том, как создать Todo-list с бесконечной вложенностью подзадач. Для создания приложения я буду использовать React, TypeScript и Mobx. С полным кодом вы можете ознакомиться в репозитории проекта.
Задачи
- Возможность добавлять новые задачи в список дел;
- Каждая задача состоит из заголовка и опционального описания;
- Возможность добавлять неограниченное количество подзадач в любую существующую задачу;
- Возможность удалять задачи из списка. Удаление задачи верхнего уровня приводит к удалению всех вложенных подзадач;
- Возможность изменять статус задачи. Изменение статуса задачи верхнего уровня устанавливает аналогичный статус для всех вложенных подзадач;
- При клике на задачу, можно просматривать подробную информацию о задаче.
Настройка проекта
Создадим React-проект с TypeScript-шаблоном, выполнив в терминале следующую команду: npx create-react-app infinite-todo-list --template typescript
.
После завершения установки перейдем в директорию проекта и установим Mobx (npm install mobx mobx-react-lite
).
В директории src оставим следующие файлы: index.tsx, App.tsx, index.css, react-app-env.d.ts. Создадим файловую структуру проекта, папки: components, store, utils, types, style, assets. В папку assets сложим необходимые иконки.
Создание UI компонентов
Нам потребуются: Button, Checkbox, Input, TextArea, Header и ModalWindow.
Начнем с компонента Header. В качестве props он будет принимать функцию переключатель для ModalWindow. Возвращать будет header с логотипом, названием приложения и кнопкой добавления новой задачи.
Далее создадим компонент Button. В качестве props он будет принимать только текст для кнопки. Все остальные свойства мы будем получать от html-элемента button, используя встроенный тип ComponentPropsWithoutRef.
Аналогичным образом создаем остальные компоненты.
Компонент Checkbox.
Компонент Input.
И компонент TextArea.
Теперь создадим компонент ModalWindow. В качестве props он будет принимать функцию переключатель и children. Возвращать будет блок div с компонентами Input (для заголовка задачи), TextArea (для описания задачи) и Button (для закрытия окна).
В App импортируем компоненты ModalWindow, Header, и Button. Создаем локальное состояние isModalShown и функцию-переключатель этого состояния.
Используем условный рендеринг, если isModalShown true, то будем отображать ModalWindow. В качестве children для ModalWindow передадим компонент Button (кнопку для добавления задач).
Создание вспомогательных функций
Работу над логикой приложения начнем с описания типа для Todo-листа. В папке types создадим одноименный файл и экспортируем из него TodoType.
Далее в папке utils создаем файл utils.ts, в котором создадим вспомогательные функции. Нам нужны функции для добавления подзадачи, удаления задачи, изменения состояния задачи и выбора задачи. Так как список у нас бесконечной вложенности, то все эти функции будут рекурсивными.
Импортируем тип TodoType и опишем типы для наших функций.
Теперь приступим к созданию самих функций.
Функция subTaskAdding будет использоваться для добавления подзадач. В качестве аргументов она будет принимать id задачи, в которую добавляется подзадача, массив типа TodoType и саму подзадачу. Возвращать будет новый массив типа TodoType. Внутри функции мы будем проходить по массиву, используя метод reduce, и сравнивать id элемента с id задачи. Если id равны, тогда добавляем задачу в массив подзадач данного элемента. Если id не равны, то вызываем эту же функцию subTaskAdding на массиве подзадач этого элемента.
Следующая функция recursionFilter будет использоваться для удаления задачи из списка. На вход данная функция будет принимать массив типа TodoType и id задачи, которую нужно удалить. Возвращать будет отфильтрованный массив типа TodoType. Внутри функции также используем метод reduce и сравниваем id элементов массива с id задачи. Если они не равны, то будем добавлять этот элемент в возвращаемый массив, а для массива подзадач будем вызывать эту же функцию recursionFilter.
Следующая функция recursionSearch будет использоваться для поиска активной (выбранной для подробного просмотра) задачи. На вход эта функция принимает массив типа TodoType и id выбранной задачи. Возвращать эта функция будет найденную задачу, либо null, если ничего не найдено. Внутри функции, воспользуемся циклом for of для прохода по массиву. Внутри цикла мы сравниваем id задачи с id элемента массива. Если они равны, возвращаем найденный элемент. Если не равны, то создаем новую переменную subItem, которой присваиваем результат вызова данной функции recursionSearch на массиве подзадач текущего элемента массива. Если subItem существует, то возвращаем его.
Изменение статуса задачи мы разделим на две функции. Первая функция recursionCompleteToggler будет изменять статус выбранной задачи. Вторая функция subTasksCompleteTogglerбудет изменять статус всех вложенных подзадач.
Функция recursionCompleteToggler будет принимать на вход массив типа TodoType и id задачи, а возвращать будет новый массив типа TodoType. Внутри функции методом reduce проходим по массиву и сравниваем id. Когда id не равны, вызываем эту же функцию recursionCompleteToggler для массива подзадач текущего элемента. Если id равны, то изменяем значение isCompleted текущего элемента на противоположное, а для массива подзадач вызываем функцию subTasksCompleteToggler.
Функция subTasksCompleteToggler в качестве аргументов принимает массив типа TodoType и значение состояния, которое необходимо установить для всех вложенных подзадач. Возвращает эта функция измененный массив типа TodoType. Внутри функции также будем использовать метод reduce. Проходим по массиву и каждому элементу для значения isCompleted задаем состояние, которое приняли в качестве аргумента. Для массива подзадач также вызываем функцию subTasksCompleteToggler.
Работа с Mobx
Теперь приступим к работе с Mobx. В папке store создадим файл todos.ts, этот файл будет содержать всю логику работы с состоянием приложения. В этот файл импортируем созданные вспомогательные функции и тип TodoType. Для генерации уникальных id для каждой задачи воспользуемся библиотекой uuid.
Создадим класс Todos и в конструкторе класса вызовем функцию makeAutoObservable, которой параметром передадим контекст текущего класса. Далее внутри класса создадим переменные состояния: todoArray, activeTask, todoTitle и todoText.
Переменные todoTitle и todoText будут отвечать за заголовок и описание новой задачи, activeTask будет содержать выбранную задачу, либо null, если задача не выбрана.
Массив todoArray будет хранить весь список задач. Сразу реализуем работу с localstorage. При создании массива todoArray, будем проверять, содержит ли localstorage ключ todos, если да, то загружаем значение из localstorage, а если такого ключа нет, то присваиваем todoArray пустой массив.
Далее приступим к созданию методов класса для работы с состоянием. Так как в Mobx состояние является изменяемым, то внутри методов мы будем просто изменять необходимые значения.
Функции titleHandler и textHandler будут в качестве аргумента принимать строку и присваивать эту строку соответствующим переменным.
Функция addTask будет формировать новый объект задачи и добавлять его в массив todoArray. После добавления новой задачи в массив, мы сохраним массив в localStorage и сбросим значения переменных todoTitle и todoText.
Функция addSubtask будет в качестве аргумента принимать id задачи, в которую необходимо добавить подзадачу. Внутри функции мы создаем объект задачи и вызываем функцию subTaskAdding, в которую передаем id, массив todoArray и только что созданный объект задачи. Результат вызова этой функции мы присваиваем в todoArray. Далее, аналогично функции addTask, сохраняем todoArray в localstorage и сбрасываем значения todoTitle и todoText.
Функция removeTask будет принимать id задачи, которую необходимо удалить. Внутри функции мы вызываем функцию recursionFilter, в которую передаем id и todoArray, результат вызова этой функции присваиваем в todoArray и сохраняем его в localstorage. После этого нам необходимо проверить, остались ли еще задачи в todoArray. Если массив пуст, то удаляем ключ todos из localstorage и сбрасываем значение activeTask.
Функция completeToggler также будет принимать id задачи. Внутри функции мы вызываем функцию recursionCompleteToggler, которой передаем id и todoArray. Результат вызова записываем в todoArray и перезаписываем localstorage.
Последняя функция chooseTask также принимает id задачи. Внутри функции мы вызываем recursionSearch, которой передаем id и todoArray. А результат выполнения присваиваем в переменную activeTask.
Осталось экспортировать экземпляр данного класса.
Создание основных компонентов
Начнем с создания компонента отдельной задачи – TodoItem. Импортируем необходимые иконки и ui – компоненты: Checkbox, ModalWindow и Button. Также нам понадобятся тип TodoType, и класс todos. В качестве props этот компонент будет принимать один объект типа TodoType, который мы сразу же деструктурируем.
Этот компонент будет использовать два локальных состояния: isModalShown будет отвечать за отображение окна добавления подзадач, а isSubTasksShown будет отвечать за отображение списка подзадач. Создадим две функции для переключения этих состояний: modalWindowToggler и subTasksToggler.
Далее воспользуемся условным рендерингом и если isModalShown – true, то будем отображать компонент ModalWindow, которому в качестве children передадим компонент Button. Событию onClick этой кнопки мы зададим метод addSubtask класса todos.
Для иконки chevronIcon в событие onClick мы назначим функцию isSubTasksShown, таким образом, при нажатии на эту иконку будет раскрываться список всех вложенных подзадач для данной задачи.
Для заголовка задачи мы также добавим событие onClick, которому зададим метод chooseTask класса todos. Таким образом, при нажатии на заголовок задачи, эта задача выберется как активная и откроется описание данной задачи.
Для иконки addIcon в событие onClick мы зададим функцию modalWindowToggler. При нажатии на эту кнопку будет отображаться окно для добавления подзадачи.
Для компонента Checkbox событию onChange назначим метод completeToggler класса todos.
Для иконки удаления deleteIcon, в событие onClick мы зададим метод removeTask класса todos.
Теперь проверим, есть ли у данной задачи вложенные подзадачи. Если длина массива subTasks больше 0, то будем методом map проходить по массиву подзадач и для каждой подзадачи отрисовывать компонент TodoItem. Таким образом мы получили рекурсивный компонент.
Для того, чтобы сделать этот компонент наблюдаемым для mobx нам необходимо импортировать функцию observer из mobx-react-lite и обернуть компонент в эту функцию.
Теперь создадим компонент TodoList, который будет служить для отображения всего списка задач. Импортируем компонент TodoItem и класс todos.
Внутри компонента просто проходим по массиву todoArray, который мы берем из класса todos и на каждый элемент массива отрисовываем компонент TodoItem. Этот компонент нам также необходимо обернуть в функцию observer из mobx-react-lite.
И создадим еще один компонент TodoDetails, который будет служить для отображения подробной информации о выбранной задаче.
Импортируем класс todos и функцию observer. Внутри компонента мы используем условный рендеринг. Будем проверять значение переменной activeTask класса todos и если значение переменной не null, то отрисовываем блок с подробной информацией о задаче.
Теперь внесем некоторые изменения в компонент ModalWindow. Импортируем функцию observer и класс todos.
Для компонентов Input и TextArea устанавливаем свойство value как todos.todoTitle и todos.todoText соответственно. Также для обоих этих компонентов зададим обработчик события onChange: для Input это будет метод titleHandler класса todos, а для компонента TextArea метод textHandler.
Осталось добавить компоненты TodoList и TodoDetails в App. А для компонента Button, который мы передаем в ModalWindow добавить событие onClick, которому задать метод addTask класса todos.
Приложение полностью готово. С полным кодом проекта вы можете ознакомиться тут, а посмотреть live-версию тут.
4К открытий8К показов