Осваиваем react-beautiful-dnd (часть 1)
Всем привет!
По итогом опроса в телеграм канале я поняла, что вам будет полезна статья по настройке и использованию библиотеки react-beautiful-dnd. Именно поэтому в этой статье мы начнем с вами создавать свой небольшой проект канбан-доски, а в ходе работы и узнаем тонкости этой библиотеки!
Давайте начнем наше знакомство с библиотекой react-beautiful-dnd.
Создание проекта
Так как мы будем разбирать эту библиотеку на практике, то давайте для начала создадим наше приложение:
npx create-react-app dnd-project cd dnd-project
Теперь можем запустить наше приложение и увидеть стандартную заставку react:
npm start
Теперь давайте почистим наш проект, оставив в файле index.js:
import React from 'react';
import ReactDOM from 'react-dom';
const App = () => <p>Hello, world!</p>
ReactDOM.render(<App />, document.getElementById('root'));Остальные файлы в папке src можно удалить, они нам не нужны.
Создадим данные для отображения
Давайте создадим файл initial-data.js и заполним его информацией:
export const initialData = {
tasks: {
'task-1': { id: 'task-1', content: 'Some text 1'},
'task-2': { id: 'task-2', content: 'Some text 2'},
'task-3': { id: 'task-3', content: 'Some text 3'},
'task-4': { id: 'task-4', content: 'Some text 4'},
'task-5': { id: 'task-5', content: 'Some text 5'},
'task-6': { id: 'task-6', content: 'Some text 6'}
},
columns: {
'column-1': {
id: 'column-1',
title: 'Надо сделать',
taskIds: ['task-1', 'task-4', 'task-5']
},
},
columnOrder: ['column-1']
}Дальше мы создадим больше колонок, но пока нам нужна только одна.
Теперь нам нужно добавить эти данные в начальный стейт нашего компонента приложения:
import React from 'react';
import ReactDOM from 'react-dom';
import { initialData } from './initial-data';
const App = () => {
const [data, _] = React.useState(initialData);
return (
<React.Fragment>
{
data.columnOrder.map(item => {
const column = data.columns[item];
const tasks = column.taskIds.map(taskId => data.tasks[taskId]);
return column.title;
})
}
</React.Fragment>
);
}
ReactDOM.render(<App />, document.getElementById('root'));Как видите тут мы просто добавили наши данные в компонент App и вызвали его отрисовку, исходя из очередности колонок. initialData поместили в стейт компонента в качестве начального значения.
Структура приложения и простые компоненты
Давайте не будет создавать код-лапшу и выделим компонент колонки в отдельный файл Column.jsx. Даже с учетом того, что это приложение не будет особо большим предлагаю сделать ему нормальную структуру, поэтому я создам папку components/Column, а в него добавлю файл Column.jsx.
В файл Column.jsx поместим простой код отображения колонки:
import React from 'react';
export const Column = ({tasks, ...props}) => {
const {id, title, taskIds} = props.columnData;
return <React.Fragment>{title}</React.Fragment>;
}А в файле index.js воспользуемся этим компонентом:
import React from 'react';
import ReactDOM from 'react-dom';
import { Column } from './components/Column/Column';
import { initialData } from './initial-data';
const App = () => {
const [data, _] = React.useState(initialData);
return (
<React.Fragment>
{
data.columnOrder.map(item => {
const column = data.columns[item];
const tasks = column.taskIds.map(taskId => data.tasks[taskId]);
return <Column key={column.id} data={column} tasks={tasks} />;
})
}
</React.Fragment>
);
}
ReactDOM.render(<App />, document.getElementById('root'));Я не хочу разводить много css файлов, так что давайте подключим styled-components:
npm i styled-components --save-dev
Еще добавим небольшую утилитку для обнуления css стилей:
npm i reset-css --save-dev
Как только в index.js добавим строчку import 'reset-css'; можно будет заметить, что все лишние отступы на странице пропали.
Теперь нужно немного украсить наши колонки, поэтому давайте доработаем компонент Column:
import React from 'react';
import styled from 'styled-components';
const Container = styled.div`
margin: 12px;
padding: 4px;
border: 1px solid lightgray;
border-radius: 4px;
`;
const Title = styled.h2`
padding: 8px;
`;
const TaskList = styled.div`
padding: 8px;
`;
export const Column = ({tasks, ...props}) => {
const {id, title, taskIds} = props.columnData;
return (
<Container>
<Title>{title}</Title>
<TaskList>TaskList</TaskList>
</Container>
);
}Если откроете браузер это будет выглядеть примерно вот так:
Дальше давайте разберемся с отрисовкой задач в колонке, для этого тоже создадим отдельный компонент src/components/Task/Task.jsx:
import React from 'react';
import styled from 'styled-components';
const Container = styled.div`
padding: 8px;
border: 1px solid lightgray;
border-radius: 4px;
margin-bottom: 8px;
`;
export const Task = ({ ...props }) => {
const { content } = props.data;
return <Container>{content}</Container>;
}Ну и в компонента Column вызовем создание компонентов Task:
import React from 'react';
import styled from 'styled-components';
import { Task } from '../Task/Task';
const Container = styled.div`
margin: 12px;
padding: 4px;
border: 1px solid lightgray;
border-radius: 4px;
`;
const Title = styled.h2`
padding: 8px;
`;
const TaskList = styled.div`
padding: 8px;
`;
export const Column = ({tasks, ...props}) => {
const {id, title, taskIds} = props.columnData;
return (
<Container>
<Title>{title}</Title>
<TaskList>
{tasks.map(task => <Task key={task.id} data={task} />)}
</TaskList>
</Container>
);
}После этого в браузере должен быть вот такой результат:
Добавим react-beautiful-dnd
Пришло время нам установить библиотеку react-beautiful-dnd:
npm i react-beautiful-dnd --save-dev
Давайте посмотрим как выглядят основные компоненты react-beautiful-dnd:
Если говорить просто, то эти компоненты отвечают за следующее:
DragDropContext- оборачиваем в этот компонент ту часть приложения, в которой хотим реализовать перетаскиванияDroppable- компонент, который определяет те места, в которые можно перетягивать и дропать другие элементыDraggable- те элементы, которые можно перетягивать и дропать
Изменения порядка элементов списка
Начнем мы с того, что обернем в DragDropContext все то, что лежит в нашем компоненте App. Но для начала давайте поймем какие пропсы нужно и можно передавать в DragDropContext:
onDragStart- колбек на момент, когда перетаскивание только началосьonDragUpdate- колбек, на изменение при перетаскиванииonDragEnd- колбек на окончание перетаскивания
Обязательным из всех этих пропсов является тольк onDragEnd, поэтому давайте уже обернем наш компонент и напишем этот колбек:
import React from 'react';
import ReactDOM from 'react-dom';
import { Column } from './components/Column/Column';
import { initialData } from './initial-data';
import { DragDropContext } from 'react-beautiful-dnd';
import 'reset-css';
const App = () => {
const [data, _] = React.useState(initialData);
const handleDragEnd = result => {
// TODO: обработать изменение порядка элементов в колонке
};
return (
<DragDropContext
onDragEnd={handleDragEnd}
>
{
data.columnOrder.map(item => {
const column = data.columns[item];
const tasks = column.taskIds.map(taskId => data.tasks[taskId]);
return <Column key={column.id} columnData={column} tasks={tasks} />;
})
}
</DragDropContext>
);
}
ReactDOM.render(<App />, document.getElementById('root'));Теперь нам надо перескочить к компоненту Column и добавить в нем Droppable:
import React from 'react';
import styled from 'styled-components';
import { Task } from '../Task/Task';
import { Droppable } from 'react-beautiful-dnd';
const Container = styled.div`
margin: 12px;
padding: 4px;
border: 1px solid lightgray;
border-radius: 4px;
`;
const Title = styled.h2`
padding: 8px;
`;
const TaskList = styled.div`
padding: 8px;
`;
export const Column = ({tasks, ...props}) => {
const {id, title} = props.columnData;
return (
<Container>
<Title>{title}</Title>
<Droppable droppableId={id}>
{(provided) => (
<TaskList
ref={provided.innerRef}
{...provided.droppableProps}
>
{tasks.map((task, idx) => <Task key={task.id} taskData={task} index={idx} />)}
{provided.placeholder}
</TaskList>
)}
</Droppable>
</Container>
);
}Как вы можете заметить, Droppable - принимает в качестве ребенка не просто реакт элемент, а функцию, которая возвращает реакт элемент, эта библиотека может себе позволить так делать, так как приложения не потеряют из-за этого в производительности, потому что react-beautiful-dnd не работает напрямую с DOM деревом.
В качестве параметра в эту функцию попадает provided, он дает нам возможность получить необходимые пропсы состояния для области драга, а так же такую штуку как placeholder - для добавления дополнительного пространства при драге в эту область.
Остался последний шаг, давайте добавим Draggable к нашему компоненту Task:
import React from 'react';
import styled from 'styled-components';
import { Draggable } from 'react-beautiful-dnd';
const Container = styled.div`
padding: 8px;
border: 1px solid lightgray;
border-radius: 4px;
margin-bottom: 8px;
backgroud-color: white;
`;
export const Task = ({ index, ...props }) => {
const { id, content } = props.taskData;
return (
<Draggable draggableId={id} index={index}>
{(provided) => (
<Container
{...provided.draggableProps}
{...provided.dragHandleProps}
ref={provided.innerRef}
>
{content}
</Container>
)}
</Draggable>
);
}В качестве дочернего элемент Draggable так же принимает функцию.
draggableProps- дает нам необходимые пропсы для элемента, который будут перетягивать.dragHandleProps- дает нам необходимые пропсы для того кусочка элемента, за который можно тянуть (в нашем случае это весь элемент).
Давайте посмотрим что у нас получилось:
Как видите красивый драг у нас работает, только вот состояние не запоминается.
Запоминаем состояние драга
Для того, чтобы сохранять состояние при драге нам надо доделать функцию handleDragEnd в нашем файле index.js, в качестве параметра в эту функцию прилетать объект вот такого типа:
{
draggableId: 'task-1',
type: 'TYPE',
reason: 'DROP',
source: {
droppableId: 'column-1',
index: 0
},
destination: {
droppableId: 'column-1',
index: 1
},
}На основе этих данных давайте доделаем нашу функцию:
const handleDragEnd = result => {
const {destination, source, draggableId } = result;
// Если объект перетащиши за область, в которую можно дропать
if (!destination) {
return;
}
// Если объект перетащили в то же самое место
if (destination.droppableId === source.droppableId
&& destination.index === source.index) {
return;
}
const column = data.columns[source.droppableId];
const newTaskIds = Array.from(column.taskIds);
newTaskIds.splice(source.index, 1);
newTaskIds.splice(destination.index, 0, draggableId);
const newColumn = {
...column,
taskIds: newTaskIds,
};
setData((prevData) => ({
...prevData,
columns: {
...prevData.columns,
[newColumn.id]: newColumn,
}
}));
};После добавление такой обработки конца драга, у нас получается работающее приложение:
Итоги
Вот мы с вами и разобрали создание самого простого перетаскивания при помощи библиотеки react-beautiful-dnd, надеюсь вам это было полезно, я планирую дальше выпустить еще пару статей, где мы разберем более сложные кейсы использования этой библиотеки!
Подпишитесь на телеграм канал, чтобы не пропустить выход новых интересных заметок