Приложение ToDo — это приложение самого начального уровня для любого фронтенд-разработчика. Базовое приложение ToDo имеет функциональность добавления, удаления и обновления дел из списка. Будучи разработчиком, мы легко забываем о задачах на день или на определенный период времени. Всегда желательно иметь такое приложение, в котором можно добавлять, удалять или изменять задачи.
В этом руководстве мы разработаем приложение ToDo App с нуля с базовой функциональностью crud (Create, Read, Update, Delete) и дополнительными возможностями, такими как поиск по фильтру, скрытие задач и обновление статуса.
Начало работы
Создавая React App из cra-шаблона с помощью create-react-app
, нам не потребуется никаких внешних библиотек для проекта, кроме react-icons
, которая нам понадобится для иконок, используемых в приложении.
ToDoApp.jsx
import React from 'react';
export default function ToDoApp() {
return (
<section className="ToDoApp">
<h1>ToDo App</h1>
</section>
);
}
Для приложения мы реализуем два компонента, а именно ToDoCard и ToDoForm.
Реализация
Добавление основных стилей
ToDoApp.css
.ToDoApp {
width: 800px;
max-width: 100%;
margin: auto;
padding: 0.5rem;
color: var(--black);
}
.grey_text {
color: var(--grey);
}
.red_text {
color: var(--red);
}
.blue_text {
color: var(--blue);
}
.green_text {
color: var(--green);
}
.ToDoApp input,
.ToDoApp textarea,
.ToDoApp select {
width: 100%;
padding: 0.5rem 0.75rem;
}
.ToDoApp textarea {
height: 10rem;
}
.ToDoApp button {
padding: 0.5rem 1.5rem;
background: var(--white);
border: 1px solid var(--black);
}
.ToDoApp__Search {
margin-top: 0.5rem;
display: flex;
gap: 1.5rem;
}
.ToDoApp__Search input {
border: 1px solid var(--black);
}
/* @ToDoList Layout */
.ToDoList {
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
margin-top: 0.5rem;
}
.ToDoList__action {
width: 100%;
}
Компонент карточки
Прежде чем начать, давайте установим react-icons
, выполнив команду
npm i react-icons
Определение json-схемы для каждого todo
{
"title": "string",
"description": "string",
"status": "integer(0,1,2)",
"hide": "boolean",
"id": "integer"
}
ToDoCard.jsx
import React from 'react';
// Icons for Todo Card
import {
FaCheckCircle,
FaClock,
FaExclamationCircle,
FaEye,
FaEyeSlash,
FaPencilAlt,
FaTimesCircle,
} from 'react-icons/fa';
export default function ToDoCard({
id,
title,
description,
status,
hide,
...otherProps
}){
// Checking if the card is to be hidden
if (hide) return null;
return (
<div className="ToDoCard" {...otherProps}>
<div className="ToDoCard__left">
<span>
{status === 0 && <FaExclamationCircle title="Pending" className="ToDoCard__icon grey_text" />}
{status === 1 && <FaClock title="Working" className="ToDoCard__icon blue_text" />}
{status === 2 && <FaCheckCircle title="Done" className="ToDoCard__icon green_text" />}
</span>
</div>
<div className="ToDoCard__center">
<h2>{title}</h2>
<p>{description}</p>
</div>
<div className="ToDoCard__right">
<FaTimesCircle
className="ToDoCard__icon red_text"
/>
<span>
<FaEye title="Show Description" className="ToDoCard__icon" />
</span>
<FaPencilAlt
className="ToDoCard__icon"
/>
</div>
</div>
);
}
Компонент ToDoCard принимает все свойства схемы ToDo, где hide используется для возврата null при true, а status показывает три разных символа при трех разных целочисленных значениях.
Кроме того, мы можем переключать описание с помощью переменной состояния,
ToDoCard.jsx
...
export default function ToDoCard({
...
}){
const [showDescription, setShowDescription] = React.useState(false);
...
return (
<div className="ToDoCard" {...otherProps}>
...
<div className="ToDoCard__center">
<h2>{title}</h2>
{showDescription && <p>{description}</p>}
</div>
<div className="ToDoCard__right">
...
<span
onClick={() => {
setShowDescription(!showDescription);
}}
>
{showDescription && <FaEye title="Show Description" className="ToDoCard__icon" />}
{!showDescription && <FaEyeSlash title="Hide Description" className="ToDoCard__icon" />}
</span>
...
</div>
</div>
);
}
Используя React.useState(), мы решаем проблему видимости описания и его переключения.
Со стилизацией карточки проблем меньше,
ToDoApp.css
...
/* @ToDo Card Layout */
.ToDoCard {
border: 1px solid var(--black);
width: 900px;
max-width: 100%;
padding: 0.5rem;
font-size: 1rem;
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.ToDoCard div {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.ToDoCard .ToDoCard__left {
flex: 0 2.5rem;
}
.ToDoCard .ToDoCard__center {
flex: 3;
display: inline-block;
}
.ToDoCard .ToDoCard__right {
flex: 0 2.5rem;
gap: 0.5rem;
}
.ToDoCard h2 {
font-size: larger;
}
.ToDoCard__icon {
cursor: pointer;
}
@media screen and (max-width: 900px) {
.ToDoCard {
width: 100%;
flex-direction: column;
}
.ToDoCard div {
flex-direction: row;
justify-content: flex-start;
}
}
Показ/скрытие карточек с ограничением
В этом разделе мы используем переменную состояния todos для хранения значений todos и переменную maxDisplayTodos для определения максимального количества видимых карточек todo.
ToDoApp.jsx
import React from 'react';
import ToDoCard from './ToDoCard';
import './ToDoApp.css';
import { FaPlusCircle } from 'react-icons/fa';
export default function ToDoApp() {
const [todos, setTodos] = React.useState([]);
const [hideTodos, setHideTodos] = React.useState(true);
const maxDisplayTodos = 5;
React.useEffect(() => {
setTodos([
{
title: 'Learn React',
description: 'Learn React and its ecosystem',
status: 0,
hide: false,
id: 1,
},
{
title: 'Create a React Component',
description:
'Lorem ipsum dolor sit, amet consectetur adipisicing elit. Veritatis esse aut similique reprehenderit fuga cupiditate porro. Nostrum, ipsam perferendis! Fuga nisi nostrum odit nulla quia, sint harum eligendi recusandae dolore!',
status: 0,
hide: false,
id: 2,
},
{
title: 'Learn Vue',
description:
'Far far away, behind the word mountains, far from the countries Vokalia and Consonantia, there live the blind texts. Separated they live in Bookmarksgrove right at the coast of the Semantics, a large language ocean. A small river named Duden flows by their place and supplies it with the necessary.',
status: 0,
hide: false,
id: 3,
},
{
title: 'Learn Angular',
description:
'A wonderful serenity has taken possession of my entire soul, like these sweet mornings of spring which I enjoy with my whole heart. I am alone, and feel the charm of existence in this spot, which was created for the bliss of souls like mine. I am so happy, my',
status: 0,
hide: false,
id: 4,
},
{
title: 'Vue Typewriter',
description:
'Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta.',
status: 0,
hide: false,
id: 5,
},
{
title: 'Learn jQuery',
description:
'Li Europan lingues es membres del sam familie. Lor separat existentie es un myth. Por scientie, musica, sport etc, litot Europa usa li sam vocabular. Li lingues differe solmen in li grammatica, li pronunciation e li plu commun vocabules. Omnicos directe al desirabilite de un nov lingua franca: On refusa',
status: 0,
hide: false,
id: 14,
},
{
title: 'Learn Javascript',
description:
'The European languages are members of the same family. Their separate existence is a myth. For science, music, sport, etc, Europe uses the same vocabulary. The languages only differ in their grammar, their pronunciation and their most common words. Everyone realizes why a new common language would be desirable: one',
status: 0,
hide: false,
id: 15,
},
]);
}, []);
function handleHideTodos() {
const newHideTodos = !hideTodos;
setHideTodos(newHideTodos);
if (newHideTodos) {
const newTodos = todos.map((todo, index) => {
if (index >= maxDisplayTodos) todo.hide = false;
return todo;
});
setTodos(newTodos);
} else {
const newTodos = todos.map((todo, index) => {
if (index >= maxDisplayTodos) todo.hide = true;
return todo;
});
setTodos(newTodos);
}
}
return (
<section className="ToDoApp">
<h1>ToDo App</h1>
<div className="ToDoList">
{(todos || []).map((todo, index) => (
<ToDoCard
key={index}
{...todo}
/>
))}
{(!todos || todos.length === 0) && (
<div className="ToDoList__empty">
<p>No todos found</p>
</div>
)}
{todos.length > maxDisplayTodos && (
<button className="ToDoList__action" type="button" onClick={() => handleHideTodos()}>
{hideTodos ? 'HIDE' : 'SHOW'}
</button>
)}
</div>
</section>
);
}
Есть еще одна переменная состояния hideTodos, используемая для определения того, когда скрывать todos, а когда нет. Также есть функция handleHideTodos(), которая обрабатывает переменную состояния hideTodos и на основе текущего состояния hideTodos мы либо скрываем, либо показываем предел maxDisplayTodos. У нас также есть индикатор no todos found для отсутствия todos и переключаемая кнопка show/hide на основе hideTodos.
Компонент формы
Прежде чем мы приступим к добавлению, редактированию и удалению записей, давайте представим наш компонент формы.
ToDoForm.jsx
import React from 'react';
import { FaTimes } from 'react-icons/fa';
function ToDoForm({
title: titleProps,
description: descriptionProps,
status: statusProps,
id,
}) {
const [title, setTitle] = React.useState(titleProps);
const [description, setDescription] = React.useState(descriptionProps);
const [status, setStatus] = React.useState(statusProps);
function handleTitleChange(e) {
setTitle(e.target.value);
}
function handleDescriptionChange(e) {
setDescription(e.target.value);
}
function handleStatusChange(e) {
setStatus(parseInt(e.target.value));
}
return (
<form className="ToDoForm">
<FaTimes className="close-btn"/>
<h2>ToDo Form</h2>
<div className="ToDoForm__field">
<label htmlFor="title">Title</label>
<input type="text" id="title" value={title} onChange={(e) => handleTitleChange(e)} />
</div>
<div className="ToDoForm__field">
<label htmlFor="description">Description</label>
<textarea
type="text"
id="description"
value={description}
onChange={(e) => handleDescriptionChange(e)}
/>
</div>
<div className="ToDoForm__field">
<label htmlFor="status">Status</label>
<select id="status" value={status} onChange={(e) => handleStatusChange(e)}>
<option value="0">Pending</option>
<option value="1">Working</option>
<option value="2">Done</option>
</select>
</div>
<div className="ToDoForm__action">
<button type="submit">{id === -1 ? 'Add' : 'Update'}</button>
</div>
</form>
);
}
ToDoForm.defaultProps = {
title: '',
description: '',
status: 0,
id: -1,
};
export default ToDoForm;
Обработка элементов формы представляет собой проблему в React, если они обрабатываются с помощью переменных состояния, нам нужно обработать inputChange с помощью обработчика событий. Поэтому есть три переменные состояния (заголовок, описание и статус) и три обработчика inputChange (handleTitleChange, handleDescriptionChange, handleStatusChange).
Стилизация компонента ToDoForm
ToDoApp.css
...
/* @ToDo Form Layout */
.ToDoForm {
padding: 0.5rem;
border: 1px solid var(--black);
margin-top: 1rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
justify-content: space-around;
position: relative;
}
.ToDoForm .close-btn {
position: absolute;
right: 0.5rem;
top: 0.5rem;
}
.ToDoForm__field,
.ToDoForm__action {
display: flex;
align-items: center;
flex-direction: row;
gap: 0.5rem;
}
.ToDoForm__field label {
flex: 0 0 6rem;
font-size: 1rem;
}
.ToDoForm__action button {
margin-left: auto;
}
Добавление компонента формы & Закрыть форму
ToDoApp.jsx
...
export default function ToDoApp(){
...
const [showForm, setShowForm] = React.useState(false);
...
return (
<section className="ToDoApp">
...
{showForm && (
<ToDoForm
closeForm={() => {
setShowForm(false);
}}
/>
)}
</section>
);
}
Добавлена переменная состояния showForm, передайте ее компоненту формы.
ToDoForm.jsx
...
function ToDoForm({
title: titleProps,
description: descriptionProps,
status: statusProps,
id,
closeForm,
)} {
...
function handleCloseForm() {
setTitle('');
setDescription('');
setStatus(0);
closeForm();
}
return (
<form className="ToDoForm">
<FaTimes className="close-btn" onClick={() => handleCloseForm()} />
...
</form>
);
}
...
Добавление обработчика для closeform с установкой всех переменных состояния в начальное состояние.
Поиск элементов Todo
ToDoApp.jsx
...
export default function ToDoApp() {
...
const [searchText, setSearchText] = React.useState('');
...
function handleSearchChange(evt) {
setSearchText(evt.target.value);
const newTodos = todos.map((todo) => {
todo.hide = !(
todo.title.toLowerCase().includes(evt.target.value.toLowerCase()) ||
todo.description.toLowerCase().includes(evt.target.value.toLowerCase())
);
return todo;
});
setTodos(newTodos);
}
return (
<section className="ToDoApp">
<h1>ToDo App</h1>
<div className="ToDoApp__Search">
<input
type="text"
value={searchText}
onChange={(evt) => handleSearchChange(evt)}
placeholder="Search"
/>
<button className="ToDoApp__create_btn">
<FaPlusCircle />
</button>
</div>
...
</section>
);
}
Используется переменная состояния searchText для хранения входного значения поиска, также обрабатывается изменение поиска со скрытием списка, который не соответствует поиску. В случае длинного списка можно было бы запросить его из базы данных с помощью загрузчика.
Добавление пунктов Todo
ToDoApp.jsx
...
export default function ToDoApp() {
...
function handleAddTodo(todo) {
const newTodo = {
title: todo.title,
description: todo.description,
status: 0,
hide: false,
id: Date.now() % 1000000,
};
setTodos([...todos, newTodo]);
setShowForm(false);
}
...
return (
<section className="ToDoApp">
<h1>ToDo App</h1>
<div className="ToDoApp__Search">
...
<button className="ToDoApp__create_btn" onClick={() => setShowForm(true)}>
<FaPlusCircle />
</button>
</div>
{showForm && (
<ToDoForm
handleAddTodo={handleAddTodo}
closeForm={() => {
setShowForm(false);
}}
/>
)}
...
</section>
);
}
Определение функции-обработчика handleAddToDo для добавления нового объекта ToDo в список ToDo и поддержание закрытия формы при отправке. Открытие формы при нажатии на кнопку создания Todo.
ToDoForm.jsx
...
function ToDoForm({
title: titleProps,
description: descriptionProps,
status: statusProps,
id,
closeForm,
handleAddTodo,
}) {
...
function handleFormSubmit(e) {
e.preventDefault();
if (title === '' || description === '') {
alert('Please fill in all fields');
return;
}
handleAddTodo({ title, description, status });
setTitle('');
setDescription('');
setStatus(0);
}
return (
<form className="ToDoForm" onSubmit={(e) => handleFormSubmit(e)}>
...
</form>
);
}
...
Определение функции handleFormSubmit для установки начальных значений и запуска обработчика addtodo.
Редактирование пункта Todo
Редактирование Todo Item немного сложнее, потому что нам нужно помнить id редактируемого элемента и его значение, передаваемое в форму todo. Давайте посмотрим, как это происходит.
ToDoApp.jsx
...
export default function ToDoApp() {
const [currentTodo, setCurrentTodo] = React.useState({});
...
function handleEditTodo(id) {
setShowForm(true);
const todo = todos.find((todo) => todo.id === id);
setCurrentTodo(todo);
}
function handleAddTodo(todo) {
if (todo.id === undefined) {
const newTodo = {
title: todo.title,
description: todo.description,
status: 0,
hide: false,
id: Date.now() % 1000000,
};
setTodos([...todos, newTodo]);
} else {
const newTodos = todos.map((todo_) => {
if (todo.id === todo_.id) {
todo_.title = todo.title;
todo_.description = todo.description;
todo_.status = todo.status;
}
return todo_;
});
setTodos(newTodos);
}
setCurrentTodo({});
setShowForm(false);
}
return (
<section className="ToDoApp">
...
{showForm && (
<ToDoForm
handleAddTodo={handleAddTodo}
{...currentTodo}
closeForm={() => {
setCurrentTodo({});
setShowForm(false);
}}
/>
)}
<div className="ToDoList">
{(todos || []).map((todo, index) => (
<ToDoCard
key={index}
{...todo}
handleEditTodo={handleEditTodo}
/>
))}
...
</div>
</section>
);
}
Добавление переменной состояния currentTodo для установки текущего объекта Todo для редактирования и передачи в качестве prop в форму ToDo, а также модификация функции handleAddTodo для обновления уже существующего объекта Todo. Добавьте функцию handleEditTodo для установки currentTodo для текущего элемента.
ToDoForm.jsx
...
function ToDoForm({
title: titleProps,
description: descriptionProps,
status: statusProps,
id,
closeForm,
handleAddTodo,
}) {
...
function handleFormSubmit(e) {
e.preventDefault();
if (title === '' || description === '') {
alert('Please fill in all fields');
return;
}
if (id >= 0) handleAddTodo({ title, description, status, id: id });
else handleAddTodo({ title, description, status });
setTitle('');
setDescription('');
setStatus(0);
}
...
}
...
Модификация функции handleFormSubmit для обработки случаев создания и обновления.
ToDoCard.jsx
...
export default function ToDoCard({
id,
title,
description,
status,
hide,
handleEditTodo,
...otherProps
}){
...
return (
<div className="ToDoCard" {...otherProps}>
...
<div className="ToDoCard__right">
...
<FaPencilAlt
className="ToDoCard__icon"
onClick={() => {
handleEditTodo(id);
}}
/>
</div>
</div>
);
}
Срабатывание функции handleEditTodo для текущего элемента ToDo.
Удалить ToDo
ToDoApp.jsx
...
export default function ToDoApp() {
...
function handleDeleteTodo(id) {
const newTodos = todos.filter((todo) => todo.id !== id);
setTodos(newTodos);
}
return (
<section className="ToDoApp">
...
<div className="ToDoList">
{(todos || []).map((todo, index) => (
<ToDoCard
key={index}
{...todo}
handleEditTodo={handleEditTodo}
handleDeleteTodo={handleDeleteTodo}
/>
))}
...
</div>
</section>
);
}
Создание функции handleDeleteTodo для id, обновление todos без данного id объекта todo и передача в ToDoCard.
ToDoCard.jsx
...
export default function ToDoCard({
id,
title,
description,
status,
hide,
handleEditTodo,
handleDeleteTodo,
...otherProps
}){
...
return (
<div className="ToDoCard" {...otherProps}>
...
<div className="ToDoCard__right">
<FaTimesCircle
className="ToDoCard__icon red_text"
onClick={() => {
handleDeleteTodo(id);
}}
/>
...
</div>
</div>
);
}
...
Элемент ToDoCard при нажатии на кнопку удаления запускает функцию handleDeleteTodo для текущего id элемента.
Изменить статус
ToDoApp.jsx
...
export default function ToDoApp() {
...
function handleChangeStatus(id) {
const newTodos = todos.map((todo) => {
if (todo.id === id) {
todo.status = todo.status === 2 ? 0 : todo.status + 1;
}
return todo;
});
setTodos(newTodos);
}
return (
<section className="ToDoApp">
...
<div className="ToDoList">
{(todos || []).map((todo, index) => (
<ToDoCard
key={index}
{...todo}
handleChangeStatus={handleChangeStatus}
handleEditTodo={handleEditTodo}
handleDeleteTodo={handleDeleteTodo}
/>
))}
...
</div>
</section>
);
}
Добавлен обработчик для changestatus для id, который передается ToDoCard для вызова. Обработчик обновляет последний статус с 0 до 2 и обратно до 0 по кругу.
ToDoCard.jsx
...
export default function ToDoCard({
id,
title,
description,
status,
hide,
handleEditTodo,
handleDeleteTodo,
handleChangeStatus,
...otherProps
}) {
...
return (
<div className="ToDoCard" {...otherProps}>
<div className="ToDoCard__left">
<span
onClick={() => {
handleChangeStatus(id);
}}
>
{status === 0 && <FaExclamationCircle title="Pending" className="ToDoCard__icon grey_text" />}
{status === 1 && <FaClock title="Working" className="ToDoCard__icon blue_text" />}
{status === 2 && <FaCheckCircle title="Done" className="ToDoCard__icon green_text" />}
</span>
</div>
...
</div>
);
}
Переданная функция для изменения статуса — это функция onclick, срабатывающая для иконки статуса, которая изменяется с изменением значения статуса.
Окончательный код
ToDoApp.css
https://github.com/shivishbrahma/nuclear-reactor/blob/main/src/ToDoApp/ToDoApp.css
ToDoApp.jsx
https://github.com/shivishbrahma/nuclear-reactor/blob/main/src/ToDoApp/ToDoApp.jsx
ToDoCard.jsx
https://github.com/shivishbrahma/nuclear-reactor/blob/main/src/ToDoApp/ToDoCard.jsx
ToDoForm.jsx
https://github.com/shivishbrahma/nuclear-reactor/blob/main/src/ToDoApp/ToDoForm.jsx