Осваиваем 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, надеюсь вам это было полезно, я планирую дальше выпустить еще пару статей, где мы разберем более сложные кейсы использования этой библиотеки!
Подпишитесь на телеграм канал, чтобы не пропустить выход новых интересных заметок