【React】使用React编写Trello风格的看板应用的步骤【react-beautiful-dnd】
首先
本次我们将使用React和react-beautiful-dnd来解释制作一个类似Trello的看板应用的步骤。虽然我也才开始学习React不到一个月,可能还有些不足之处,但如果可以的话,希望能够得到一些建议和评论。
可供参考
这篇文章是根据下面的链接进行实际操作后所创建的内容进行解释。
为了方便那些不懂英语或感到难以入门的人而写的。
顺便一提,我完全不懂英语,所以在进展中一边进行翻译和使用console.log()逐一确认数值和结果。
通过阅读这篇文章,您将能够做到以下事情
– 列表排序
– 拖放操作中样式的变化
– 理解react-beautiful-dnd内部的运作方式
– styled-components的简单写法
建立/搭建環境
首先,我们将使用create-react-app进行环境设置。
首先,在github上创建一个存储库。这次我选择了存储库名称为react-beautiful-dnd-practice-demo。
创建完成后,将其克隆到本地。
$ git clone https://github.com/shouyamamoto/react-beautiful-dnd-practice-demo
然后进入git clone的文件夹,运行create-react-app命令。
$ npx create-react-app ../react-beautiful-dnd-practice-demo
当执行完成后,我认为文件夹的结构将看起来像这样。
react-beautiful-dnd-practice-demo
|ーnode_modules
|ーpublic
|ーsrc
package-lock.json
package.json
README.md
这次我们将在src文件夹内进行工作。接下来我们将删除不必要的文件。
首先,删除src/index.js文件之外的所有文件。
接下来,让我们整理一下index.js文件。
import React from 'react';
import ReactDOM from 'react-dom';
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
);
为了确认它是否可以正常工作,让我们尝试在屏幕上输出”你好,世界”。
import React from 'react';
import ReactDOM from 'react-dom';
const App = () => 'hello world!' // 追記
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
);
然后,在终端上运行npm start命令。
$ npm start
如果浏览器自动启动并在屏幕上显示“hello world”,则环境设置就可以了✌️
试着创建并显示管理的数据
本文介绍了跟着react-beautiful-dnd的教程去解释以及创建应用程序功能所需的本地文件数据。现在,在src文件夹中创建initial-data.js。
src
|- initial-data.js
const initialData = {
tasks: {
'task1' : {id: 'task1', content: 'Take out the garbage'},
'task2' : {id: 'task2', content: 'Watch my favorite show'},
'task3' : {id: 'task3', content: 'Charge my phone'},
'task4' : {id: 'task4', content: 'Cook dinner'},
},
columns: {
'column-1': {
id: 'column-1',
title: 'Todo',
taskIds: ['task1', 'task2', 'task3', 'task4']
},
},
columnOrder: ['column-1'],
}
export default initialData
任务中使用任务的ID作为键。每个任务都包含ID和内容(垃圾处理)。
在”columns”中,我们放置了所需的列信息。
由于这一列的标题是”ToDo”,根据这篇文章您应该能够理解它指的是最左边的那一列。
“taskIds”字段还决定了任务的顺序。
columnOrder确定了以哪种顺序显示列。
首先只创建了一个列(Todo),所以只填入了一个值。之后还会创建进行中的列和列举已完成任务的列。
在index.js中导入并使用此export。
首先,编写代码来输出Todo标题,以确保它可以正确使用。
import React from 'react';
import ReactDOM from 'react-dom';
import initialData from './initial-data' // インポート
class App extends React.Component {
state = initialData // stateを設定
render() {
return this.state.columnOrder.map((columnId) => {
const column = this.state.columns[columnId]
const tasks = column.taskIds.map(taskId => this.state.tasks[taskId])
return column.title // タイトルを表示させる
})
}
}
以下略
只要浏览器中显示出“Todo”二字就可以了。
既然确认了它能正常工作,下一步我们将开始列举任务。
此外,我将不会详细解释关于JavaScript和React的描述,但我打算适当地加入注释来进行解释(我还会贴上一些链接)。如果你对所写的处理过程不了解,可能是因为你的JavaScript基础知识或React基础知识不足,所以我建议你重新从基础开始学习,然后再回来或者在学习过程中查找相关内容。
回到index.js,编写代码以渲染Column组件。
class App extends React.Component {
state = initialData
render() {
return this.state.columnOrder.map((columnId) => {
const column = this.state.columns[columnId]
const tasks = column.taskIds.map(taskId => this.state.tasks[taskId])
// 追記
return <Column key={column.id} column={column} tasks={tasks} />
})
}
}
以下略
既定では缺少Column组件,因此需要创建一个。
现在创建src/Column.jsx文件。
src
|- initial-data.js
|- Column.jsx // 新規作成
import React from 'react';
export default class Column extends React.Component {
render() {
return (
this.props.column.title
)
}
}
然后,让我们在index.js中导入Column组件。
import React from 'react';
import ReactDOM from 'react-dom';
import initialData from './initial-data'
import Column from './Column' //追記
以下略
然后,我相信浏览器中的错误会消失,待办事项会显示出来。
使用styled-components来应用样式。
首先,我们要在项目中安装styled-components。
$ npm install styled-components
为了在浏览器之间保持一致的样式,请考虑引入重置CSS。
$ npm install @atlaskit/css-reset
如果安装成功,首先回到index.js,然后逐步导入重置CSS。
import React from 'react';
import ReactDOM from 'react-dom';
import '@atlaskit/css-reset' // 追加
import initialData from './initial-data'
import Column from './Column'
然后我认为风格变了。
下一步我们将使用styled-components来应用样式。
首先导入styled-components,并定义要应用样式的组件。
import React from 'react';
import styled from 'styled-components' // 追加
const Container = styled.div`` // 全体を囲むdiv
const Title = styled.h3`` // Todoなどのタイトルを表すh3
const TaskList = styled.ul`` // Taskを囲むul
export default class Column extends React.Component {
render() {
return (
this.props.column.title
)
}
}
然后将要应用于每个组件的样式写下来。
const Container = styled.div`
margin: 8px;
border: 1px solid lightgray;
border-radius: 2px;
`
const Title = styled.h3`
padding: 8px;
`
const TaskList = styled.ul`
padding: 8px;
`
接下来,将其写入JSX中。
import React from 'react';
import styled from 'styled-components'
const Container = styled.div`
margin: 8px;
border: 1px solid lightgray;
border-radius: 2px;
`
const Title = styled.h3`
padding: 8px;
`
const TaskList = styled.ul`
padding: 8px;
list-style: none;
`
export default class Column extends React.Component {
render() {
return (
<Container>
<Title>{this.props.column.title}</Title>
<TaskList>ここにタスクが入る</TaskList>
</Container>
)
}
}
然后,以适用的样式显示在浏览器中。
这是基本的styled-components的使用方法。
逐个显示Todo
我們將在這個區塊中顯示待辦事項的處理過程。
import Task from './Task' // 追加
export default class Column extends React.Component {
render() {
return (
<Container>
<Title>{this.props.column.title}</Title>
<TaskList>
{this.props.tasks.map(task => <Task key={task.id} task={task} />} //追加
</TaskList>
</Container>
)
}
}
接下来,我们将创建一个名为Task的组件。
src
|- initial-data.js
|- Column.jsx
|- Task.jsx // 新規追加
import React from 'react'
import styled from 'styled-components'
const List = styled.li`
padding: 8px;
margin-bottom: 8px;
border: 1px solid rightgray;
border-radius: 2px;
`
export default class Task extends React.Component {
render() {
return(
<List>
{this.props.task.content}
</List>
)
}
}

使用react-beautiful-dnd进行列表排序。
react-beautiful-dnd的简介

第一个是DragDropContext。
这是一个使拖放(以下简称dnd)成为可能的区域,是最外层的区域,用于表达“这个区域可以进行dnd操作”。
第二个是“可拖动(Droppable)”。它表示了可拖动的区域。
第三个选项是可拖动 (Draggable)。它表示的是可拖动的区域。
通过围绕这三个组件来实现所需的拖放功能,我们可以实现“从这里到这里都可以进行拖放”,“这里是可放置拖动物体的位置”,“这里是可拖动的位置”等功能。
顺便提一下,虽然在这篇文章中无法实现拖动功能,但根据官方github的教程进行操作,似乎可以让这个区域也能拖动。
关于这方面的内容,我们打算以后再做。
在中文中实现react-beautiful-dnd。
首先,将react-beautiful-dnd引入到项目中。
$ npm install react-beautiful-dnd
然后,我们将进行导入。
需要导入的文件是需要包围在元素中的文件。
在这个例子中,我们需要将其导入到index.js文件中。
import { DragDropContext } from "react-beautiful-dnd";
然后,让我们用来围绕想要进行拖放的区域。
class App extends React.Component {
state = initialData
render() {
return (
<DragDropContext>
{this.state.columnOrder.map(columnId => {
const column = this.state.columns[columnId]
const tasks = column.taskIds.map(taskId => this.state.tasks[taskId])
return <Column key={column.id} column={column} tasks={tasks} />
})}
</DragDropContext>
)
}
}
在中,您可以使用三个回调函数。
・onDragStart: 当开始拖动时被调用(当物体被拿起时)
・onDragUpdate: 当拖动过程中有任何变化发生时被调用,例如将物体移动到新位置
・onDragEnd: 当拖动结束时被调用(当物体被放下时)
在这个回调函数中一定会需要的是onDragEnd。
class App extends React.Component {
state = initialData
render() {
return (
<DragDropContext // 3つのコールバックを取れる
onDragStart={} // 開始時
onDragUpdate={} // 途中
onDragEnd={} // 終了時 **絶対に必要**
>
{this.state.columnOrder.map(columnId => {
const column = this.state.columns[columnId]
const tasks = column.taskIds.map(taskId => this.state.tasks[taskId])
return <Column key={column.id} column={column} tasks={tasks} />
})}
</DragDropContext>
)
}
}
在保留onDragEnd的情况下,删除其他两个回调函数。然后,在onDragEnd中,将在index.js中定义的方法传递给它。然后返回这里,继续进行下一步。
const onDragEnd = result => {}
render() {
return(
...
<DragDropContext onDragEnd={this.onDragEnd}>
...
</DragDropContext>
)
}
下一步,我们将在中放置可放置的区域。这个可放置的区域是Column组件。
import { Droppable } from "react-beautiful-dnd";
export default class Column extends React.Component {
render() {
return (
<Container>
<Title>{this.props.column.title}</Title>
<Droppable> //ここと追加
<TaskList>
{this.props.tasks.map(task => <Task key={task.id} task={task} />)}
</TaskList>
</Droppable> //ここに追加
</Container>
)
}
}
这个可拖拽元素需要一个唯一的ID。这是因为当元素在不同的列之间移动时,需要识别它属于哪一列。
假设我们将Todo的ID设为column-1,done的ID设为column-2。
当我们注册一个“垃圾清理”的Todo时,由于当前垃圾清理任务在Todo中,所以用ID表示就是“在column-1的列中有一个垃圾清理任务”。
假设您已完成垃圾倒掉并将任务从“待办事项”拖放到“已完成”栏。用ID表示,这意味着“在column-1列中的垃圾倒掉任务已移动到column-2”。
如果存在ID的话,在移动它的地方会产生重复,结果就无法知道它移动到哪里了。因此,需要设置一个唯一的ID。
在这种情况下,我们将利用在inisial-data.js文件中的columns字段中设置的值column-1。
将ID设置给dorpppableId。
<Droppable droppableId={this.props.column.id}>
</Droppable>
此外,您需要将其包装成一个具有provided参数的函数。
根据provided参数的值,您可以追踪哪个项目被移动到哪个位置。
(非常抱歉无法详细解释这个过程,我会努力学习的。有关ref的更多信息,请参考此处。)
然后,我们需要再添加一个重要的占位符。
这是为了增加可以拖放的区域而必需的。占位符应作为指定为可拖放的组件的子组件添加。
另外,我们正在使用map函数对tasks进行迭代,为了确定这个task所在的索引,我们需要将索引作为第二个参数设置,并将其传递给子组件。
export default class Column extends React.Component {
render() {
return (
<Container>
<Title>{this.props.column.title}</Title>
<Droppable droppableId={this.props.column.id}>
{(provided) => ( // 追加
<TaskList
ref={provided.innerRef} // 追加
{...provided.droppableProps} // 追加
>
{this.props.tasks.map((task, index) => <Task key={task.id} task={task} index={index}/>)} // indexを追加
{provided.placeholder} // 追加
</TaskList>
)}
</Droppable>
</Container>
)
}
}
接下来,我们将编写代码以使Task组件可拖动。
首先是导入部分,然后用将元素包围起来,并设置draggableId。
此外,将其封装为一个具有与Column组件相同的provided参数的函数,以便可以进行跟踪。
此次新增了{…provided.dragHandleProps}这句话。通过添加这句话,可以使可拖动的组件可以被拖动。
import { Draggable } from 'react-beautiful-dnd' // 追加
export default class Task extends React.Component {
render() {
return(
<Draggable
draggableId={this.props.task.id} // 追加
index={this.props.index} // 追加
>
{(provided) => ( // 追加
<List
ref={provided.innerRef} // 追加
{...provided.draggableProps} // 追加
{...provided.dragHandleProps} // 追加
>
{this.props.task.content}
</List>
)}
</Draggable>
)
}
}
如果能够描述到此,我认为可以实现元素的拖拽功能。但是,存在一个问题。这个问题是拖拽的元素会自动回到原来的位置。下面我们来解决这个问题。
使用onDragEnd回调函数将列表排序持久化。
接下来回到index.js文件,在onDragEnd回调函数中进行处理。
关于结果
在介绍之前,我将解释一下result中包含的值。
这段代码是一个例子,所以不需要创建新文件,但是如果你自己使用console.log(result)等函数来确认其中的内容,你会更加理解它(就像我一样)。
const result = {
draggableId: 'task-1',
type: 'TYPE',
reason: 'DROP',
source: {
droppableId: 'column-1',
index: 0,
},
destination: {
droppableId: 'column-2',
index: 1,
}
}
这个result对象包含了onDragEnd的结果信息,即拖动结束后的数据。下面只对重要属性进行解释。
・可拖动元素的id:draggableId
・源droppableId: 在放置前的列id
・源index: 在放置前的索引
・目的droppableId: 在放置后的列id
・目的index: 在放置后的索引
通过使用这些方法,实现交互的持久化。
以一种图像的方式,获取被拖动元素位于哪个列的哪个索引,以及被拖放到哪个列的哪个索引。然后,通过改变状态,React检测到状态的变化,并进行DOM的重写等操作。
我们来编写onDragEnd函数的处理代码吧。
拖拽结束的处理方式
请参考我的注释并尝试编写代码,我将在处理过程中逐步提供。
onDragEnd = result => {
const { draggableId, source, destination } = result
// destination: ドロップ先 がない場合には処理を終了
if(!destination) {
return
}
// ドロップ前後のIDが同じで、ドロップ前後のindexが同じ場合には処理を終了
if(
destination.droppableId === source.droppableId &&
destination.index === source.index
) {
return
}
// dndした要素カラムIdから、元の場所にあったカラムを取得(state)
const column = this.state.columns[source.droppableId]
// taskIdsのコピー
const newTasksIds = Array.from(column.taskIds)
// 配列からドラックした要素から1つ削除する
newTasksIds.splice(source.index, 1)
// ドロップした場所に、ドロップした要素を追加する
newTasksIds.splice(destination.index, 0, draggableId)
// columnをコピーし、taskIdsを上書き
const newColumn = {
...column,
taskIds: newTasksIds,
}
// stateをコピーし、columnsを上書き
const newState = {
...this.state,
columns: {
...this.state.columns,
[newColumn.id]: newColumn
},
}
// stateの更新
this.setState(newState)
}
如果你已经实现到这一步,请确认一下应用。你会发现任务的移动已经被永久保存了。
这样,你暂时完成了一个只能拖放一个列的todo应用程序的制作。
接下来,让我们尝试一些方法来更加美观地展示拖放功能。
使用 react-beautiful-dnd 的快照来更改拖曳过程中的样式。
Draggable和Droppable有各自的对象,称为snapshot。snapshot包含便利的属性,可以用来改变拖动过程中的样式。
快照属性的说明
现在让我们来看一下属性。
// Draggable
const draggableSnapshot = {
isDragging: boolean,
draggingOver: 'column-1'
}
// Droppable
const droppableSnapshot = {
isDraggingOver: boolean,
draggingOverWith: 'task-1'
}
这是分别设置在可拖拽组件和可放置组件的快照。
首先,对于可拖动的快照(draggableSnapshot)中的isDragging属性,它会取一个布尔值作为值。当可拖动组件正在被拖动时,该值会变为true。在本应用中,它表示了移动任务的时机。
接下来,对于可拖动快照(draggableSnapshot)中的draggingOver属性,当可拖动组件位于可接受拖放的组件上时,该属性值将成为可接受拖放组件的ID。
另外,当可拖动组件位于不可接受拖放的区域上时,该属性值将变为null。
droppableSnapshot的isDraggingOver属性,表示当可拖拽组件位于可放置组件之上时为true。
另外,droppableSnapshot的draggingOverWith属性,存储了位于可放置组件之上的可拖拽组件的id。
尝试使用快照功能
使用方法很简单。
可拖动组件的处理
首先,将其移动到允许拖拽的组件中,即移动到Task.jsx中。并且,在{(provided, snapshot) => (…)}之后,继续添加snapshot参数到provided之后。
然后,对于可拖拽的组件,添加isDragging={snapshot.isDragging}。
最后,在styled-component中,只需通过条件判断来改变颜色即可✌️。
import React from 'react'
import styled from 'styled-components'
import { Draggable } from 'react-beautiful-dnd'
const List = styled.li`
padding: 8px;
margin-bottom: 8px;
border: 1px solid lightgray;
border-radius: 2px;
// 条件分岐でスタイルを変更させる
background-color: ${props => (props.isDragging ? 'lightgreen' : 'white' )};
`
export default class Task extends React.Component {
render() {
return(
<Draggable draggableId={this.props.task.id} index={this.props.index}>
{(provided, snapshot) => ( // ここにsnapshotを追加
<List
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
isDragging={snapshot.isDragging} // ここにisDraggingを追加
>
{console.log(provided.dragHandleProps)}
{this.props.task.content}
</List>
)}
</Draggable>
)
}
}
当在浏览器中确认时,我认为拖动时背景色会变成轻绿色,放置后会变为白色。
像这样,我们可以获取可拖动组件的状态并改变样式。
可拖拽组件的处理方法
接下来,我们将转向可拖放的组件,也就是Column.jsx。
因为之前的步骤和这次差别不大,所以我们跳过解释。
import React from 'react';
import Task from './Task'
import styled from 'styled-components'
import { Droppable } from 'react-beautiful-dnd'
const Container = styled.div`
margin: 8px;
border: 1px solid lightgray;
border-radius: 2px;
`
const Title = styled.h3`
padding: 8px;
`
const TaskList = styled.ul`
padding: 8px;
list-style: none;
min-height: 200px;
// スタイルを追加
transition: background-color 0.2s ease;
background-color: ${props => (props.isDraggingOver ? 'skyblue' : 'white')};
`
export default class Column extends React.Component {
render() {
return (
<Container>
<Title>{this.props.column.title}</Title>
<Droppable droppableId={this.props.column.id}>
{(provided, snapshot) => ( // snapshotを追加
<TaskList
ref={provided.innerRef}
{...provided.droppableProps}
isDraggingOver={snapshot.isDraggingOver} // isDraggingOverを追加
>
{this.props.tasks.map((task, index) => <Task key={task.id} task={task} index={index}/>)}
</TaskList>
)}
</Droppable>
</Container>
)
}
}
在这种状态下进行确认时,当拖动时,背景色会变为天蓝色,而放下时会变成白色。
提供了在列之间移动任务的功能。
那么,让我们添加一列吧。
由于列是在initial-data.js中管理的,所以我们要进行移动。
const initialData = {
tasks: {
'task1' : {id: 'task1', content: 'Take out the garbage'},
'task2' : {id: 'task2', content: 'Watch my favorite show'},
'task3' : {id: 'task3', content: 'Charge my phone'},
'task4' : {id: 'task4', content: 'Cook dinner'},
},
columns: {
'column-1': {
id: 'column-1',
title: 'Todo',
taskIds: ['task1', 'task2', 'task3', 'task4']
},
'column-2': { // 追加
id: 'column-2',
title: 'progress',
taskIds: []
},
'column-3': { // 追加
id: 'column-3',
title: 'done',
taskIds: []
},
},
columnOrder: ['column-1', 'column-2', 'column-3'], // 追加
}
export default initialData
我在这里添加了两列「进行中」和「已完成」。id需要保持唯一性,而taskIds则保持空状态。
然后,通过columnOrder指定了顺序。
这样,浏览器就会显示出三个列「待办事项」「进行中」「已完成」。
整理文本组的样式
由于目前三列是垂直排列的,让我们将它们改为横向排列。我们将把它们移到index.js文件中。
import styled from 'styled-components'
const Container = styled.div`
display: flex;
`
render() {
return (
<DragDropContext
onDragEnd={this.onDragEnd}
>
<Container>
{this.state.columnOrder.map(columnId => {
const column = this.state.columns[columnId]
const tasks = column.taskIds.map(taskId => this.state.tasks[taskId])
return <Column key={column.id} column={column} tasks={tasks} />
})}
</Container>
</DragDropContext>
)
}
这样就是横排了,但是因为没有指定宽度,所以需要修改。
const Container = styled.div`
margin: 8px;
border: 1px solid lightgray;
border-radius: 2px;
width: 200px;
`
而且,即使在当前阶段尝试移动任务,我认为背景颜色也不会延伸到整个屏幕。这是因为进度列和完成列没有高度。为了解决这个问题,我们需要应用样式。
const Container = styled.div`
margin: 8px;
border: 1px solid lightgray;
border-radius: 2px;
width: 220px;
display: flex; // 追加
flex-direction: column; //追加
`
const Title = styled.h3`
padding: 8px;
`
const TaskList = styled.ul`
list-style: none;
padding: 8px;
transition: background-color 0.3s ease;
background-color: ${props => (props.isDraggingOver) ? 'skyblue' : 'white'};
flex-grow: 1; // 追加
min-height: 100px; // 追加
`
我想,现在当鼠标悬停在Progress和Done列上时,背景颜色应该会显示出来了。
编写一个可以在列之间移动的过程。
我认为现在将任务移动到Progress栏或Done栏中无法正常工作。需要修正index.js中的onDragEnd处理。
onDragEnd = result => {
const { draggableId, source, destination } = result
// この部分はsource.droppableIdとなっており、今回は開始時と終了時のカラムが違う
// 可能性(TodoカラムからDoneカラムへ移動するなど)があるため、書き換える必要がある。
const column = this.state.columns[source.droppableId]
const newTasksIds = Array.from(column.taskIds)
newTasksIds.splice(source.index, 1)
newTasksIds.splice(destination.index, 0, draggableId)
const newColumn = {
...column,
taskIds: newTasksIds,
}
const newState = {
...this.state,
columns: {
...this.state.columns,
[newColumn.id]: newColumn
},
}
this.setState(newState)
}
首先,写下一个获取开始列和结束列的处理过程,如果开始列和结束列相同,那么就执行以前的处理方式进行改写。
onDragEnd = result => {
const { draggableId, source, destination } = result
const start = this.state.columns[source.droppableId] // 開始時のカラムを取得
const finish = this.state.columns[destination.droppableId] // 終了時のカラムを取得
if(start === finish) { // 開始時のカラムと終了時のカラムが同じであれば実行
const newTasksIds = Array.from(start.taskIds) // columnからstartに書き換え
newTasksIds.splice(source.index, 1)
newTasksIds.splice(destination.index, 0, draggableId)
const newColumn = {
...start, // columnからstartに書き換え
taskIds: newTasksIds,
}
const newState = {
...this.state,
columns: {
...this.state.columns,
[newColumn.id]: newColumn
},
}
this.setState(newState)
return // ここを通ったら処理を終わらせる
}
}
在同一列中,任务的移动应该仍然是可行的。
最后,将编写一段代码,使任务能够在不同的列中移动。这里的代码应该与已经编写的代码非常相似,因此很容易理解。
// 開始カラムと終了カラムが違い場合の処理
const startTasksIds = Array.from(start.taskIds) // 開始時のカラムからタスクのidを取得
startTasksIds.splice(source.index, 1) // draggableなコンポーネントが元あった場所から1つ配列を削除する
const newStart = {
...start, // 開始時のカラムをコピー
taskIds: startTasksIds, // taskIdsの値をstartTaskIdsに置き換える
}
const finishTasksIds = Array.from(finish.taskIds) // 終了時のカラムからタスクのidを取得
finishTasksIds.splice(destination.index, 0, draggableId) // draggableなコンポーネントが置かれた場所にdraggableなコンポーネントを追加する
const newFinish = {
...finish, // 終了時のカラムをコピー
taskIds: finishTasksIds, // taskIdsの値をfinishTaskIdsに置き換える
}
const newState = {
...this.state, // stateをコピー
columns: {
...this.state.columns, // columnsをコピー
[newStart.id]: newStart, // start時のid: {id: start時のid, title: 'start時のtitle', taskIds: 'start時のids'}
[newFinish.id]: newFinish, // finish時のid: {id: finish時のid, title: 'finish時のtitle', taskIds: 'finish時のids'}
}
}
this.setState(newState) //stateの更新→再レンダリング
请在浏览器中进行动态检查。如果一切正常运行,那就完成了!恭喜!最后我将附上index.js的完整代码。
import React from 'react';
import ReactDOM from 'react-dom';
import '@atlaskit/css-reset'
import styled from 'styled-components'
import { DragDropContext } from 'react-beautiful-dnd'
import initialData from './initial-data'
import Column from './Column'
const Container = styled.div`
display: flex;
`
class App extends React.Component {
state = initialData
onDragEnd = result => {
const { draggableId, source, destination } = result
// destination: ドロップ先 がない場合には処理を終了
if (!destination) {
return
}
// ドロップ前後のIDが同じで、ドロップ前後のindexが同じ場合には処理を終了
if (
destination.droppableId === source.droppableId &&
destination.index === source.index
) {
return
}
const start = this.state.columns[source.droppableId] // 開始時のカラムを取得
const finish = this.state.columns[destination.droppableId] // 終了時のカラムを取得
if (start === finish) { // 開始時のカラムと終了時のカラムが同じであれば実行
const newTasksIds = Array.from(start.taskIds) // columnからstartに書き換え
newTasksIds.splice(source.index, 1)
newTasksIds.splice(destination.index, 0, draggableId)
const newColumn = {
...start, // columnからstartに書き換え
taskIds: newTasksIds,
}
const newState = {
...this.state,
columns: {
...this.state.columns,
[newColumn.id]: newColumn
},
}
this.setState(newState)
return
}
// 開始カラムと終了カラムが違い場合の処理
const startTasksIds = Array.from(start.taskIds) // 開始時のカラムからタスクのidを取得
startTasksIds.splice(source.index, 1) // draggableなコンポーネントが元あった場所から1つ配列を削除する
const newStart = {
...start, // 開始時のカラムをコピー
taskIds: startTasksIds, // taskIdsの値をstartTaskIdsに置き換える
}
const finishTasksIds = Array.from(finish.taskIds) // 終了時のカラムからタスクのidを取得
finishTasksIds.splice(destination.index, 0, draggableId) // draggableなコンポーネントが置かれた場所にdraggableなコンポーネントを追加する
const newFinish = {
...finish, // 終了時のカラムをコピー
taskIds: finishTasksIds, // taskIdsの値をfinishTaskIdsに置き換える
}
const newState = {
...this.state, // stateをコピー
columns: {
...this.state.columns, // columnsをコピー
[newStart.id]: newStart, // start時のid: {id: start時のid, title: 'start時のtitle', taskIds: 'start時のids'}
[newFinish.id]: newFinish, // finish時のid: {id: finish時のid, title: 'finish時のtitle', taskIds: 'finish時のids'}
}
}
this.setState(newState) //stateの更新→再レンダリング
}
render() {
return (
<DragDropContext
onDragEnd={this.onDragEnd}
>
<Container>
{this.state.columnOrder.map(columnId => {
const column = this.state.columns[columnId]
const tasks = column.taskIds.map(taskId => this.state.tasks[taskId])
return <Column key={column.id} column={column} tasks={tasks} />
})}
</Container>
</DragDropContext>
)
}
}
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
);
总结
在阅读到这里时,非常感谢您的写作。辛苦了。
这次我第一次写了一篇关于React的文章。
最初我打算用JavaScript来实现,但是我放弃了,因为估计会花费很长时间,还好没有去做…
在工作中,人们一定程度上会要求我们能够迅速而精美地完成写作,所以我认为明智的选择是充分利用可用的工具,将资源集中用于其他事情。我从中学到了这一点。
在代码方面,我学到了一些关于数组处理方法、展开运算符的用法以及styled-components的写法。确实,看实际由专业人士编写的代码也很重要。
我还看过了一门用英语解说的课程,即使听不懂他们在说什么,通过观察代码可以理解他们想做什么,并且通过翻译也能勉强搞定。另外,非常重要的一点就是运行console.log并逐一确认其内容。我认为这是最重要的。
未来的发展
・提供一个输入区域,使用户可以添加值
・希望与Firebase协作,以便可以保存TODO等等
・因为使用了class组件的写法,所以想要改写成函数组件,或者使用React hooks进行改写
・由于界面非常简单,所以希望也可以使用Material UI等组件库
・希望能够让真正的职场使用者使用,并进行改进和修正
・目前只实现了CRUD中的C,希望能添加其他功能。
广告
我正在念叨更新。
博客 → shoublog
推特 → syoyamamoto
我正在摩里肯塾学习中。我在这里学习了JavaScript的基础。必须关注Moriken老师。
Moriken老师的推特账号→ @terrace_tech
Moriken老师的博客→ 無骨日记
此外,我也在Udemy上受到知名的はむ先生的社群學習。在那裡有很多現役工程師,我一直受到他們的照顧。はむ先生是一位網頁工程師,從金融危機的裁員中重新崛起。歡迎從他的個人資料進入はむ社群。