はじめに
今回、Reactとreact-beautiful-dndを使って、Trello風のカンバンアプリを制作する手順を解説します。
といっても私もまだReactを学び始めて1ヶ月程度ですので至らない点もあるとは思いますが、よかったらアドバイスやコメントなどいただけますと幸いです。
参考
こちらの記事は下のリンクを参考にハンズオンで作成したものを解説しています。
英語なのでわからない、とっつきにくいといった方にむけて書いています。
ちなみに僕は全く英語がわからないので、翻訳やconsole.log()
で値や結果を逐一確認しながら進めていきました。
この記事を見てできるようになること
・リストの並び替え
・ドラッグ&ドロップ動作中のスタイル変更
・react-beautiful-dndが中でどのような動作をしているのかの理解
・styled-componentsの簡単な記述方法
環境構築
まず、環境構築はcreate-react-app
で行います。
はじめにgithubでリポジトリを作成します。今回リポジトリ名はreact-beautiful-dnd-practice-demo
としました。
作成できたらそれをgit clone
します。
$ 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')
);
そして、きちんと動作するかを確認するために画面にhello world
を出力してみましょう。
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
と表示されていれば環境構築はOKです✌️
管理するデータを作成して表示してみる
今回の記事では、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
tasks
にはキーとしてタスクのIDを設定しています。また、それぞれのタスクにはidと内容(ゴミ出し)が含まれています。
columns
には、列に必要な情報をいれています。
この列のタイトルはTodo
なので、この記事を見てできるようになることで見た一番左の列を指していることがわかるかと思います。
またtaskIds
ではタスクの順序を決めています。
columnOrder
には列をどの順番で表示させるかを決めています。
まずは1列(Todo)のみを作成するので、1つしか値をいれていません。のちのち進行中のカラムや、完了したタスクを列挙するカラムも作成していきます。
そして、これをexport
してindex.jsでimport
して使います。
まずは正しく使えるかを確認するために、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
と表示されていればOKです。
ではただしく動作していることがわかったので、次はタスクを列挙していきます。
また、ここから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
しましょう。
import React from 'react';
import ReactDOM from 'react-dom';
import initialData from './initial-data'
import Column from './Column' //追記
以下略
するとブラウザでのエラーが消え、Todoが表示されていると思います。
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
を使ってスタイルを当てていきます。
インポートして、スタイルを当てたいコンポーネントを定義します。
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を1件ずつ表示させていく
<TaskList>ここにタスクが入る</TaskList>
の部分に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>
)
}
}
これでいったんはTodoを表示させることができました。
このような表示になっていればOKです✌️
react-beautiful-dndでリストを並び替える
react-beautiful-dndの概要
まず手を動かす前にreact-beautiful-dnd
がどのようなコンポーネントで構成されているかを見ていきましょう。
こちらはreact-beautiful-dnd
から引用させていただいたものです。
react-beautiful-dnd
は3つのコンポーネントから構成されます。
1つ目は、DragDropContext
です。
これは、ドラッグアンドドロップ(以下:dnd)を可能にするためのもので、「この領域はdndできるようにしたるで」
の一番外側の領域です。
2つ目は、Droppable
です。これはドロップ可能な領域
を表しています。
3つ目は、Draggable
です。これは ドラッグ可能な領域
を表しています。
これら3つのコンポーネントでdndしたい要素を囲っていくことで、「ここからここまでdndできるようになるで」、「ここはドラッグしたものを置ける場所やで」、**「ここはドラッグできる場所やで」**ということができるようになります。
ちなみに、Droppable
は、今回の記事ではドラッグすることはできませんが、公式のgithubのチュートリアル通りに進めていけば、ここの領域もドラッグできるようになるみたいです。
その辺りは追々やっていこうと思っています。
react-beautiful-dndの実装
まずはプロジェクトにreact-beautiful-dnd
を入れます。
$ npm install react-beautiful-dnd
そして、インポートしていきます。
インポートするファイルですが、これは<DragDropContext>
で囲む要素があるファイルにインポートしましょう。
今回でいえば、index.js
になります。
import { DragDropContext } from "react-beautiful-dnd";
そして、<DragDropContext>
で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>
)
}
}
<DragDropContext>
には、3つのコールバックをとることができます。
・onDragStaer
: ドラッグ開始時に呼ばれる(持ったとき)
・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
のみを残してその他2つのコールバックは削除しておきましょう。
そして、onDragEnd
には、index.js内で定義したメソッドを渡してあげます。またここに戻ってくるので、次に進みます。
const onDragEnd = result => {}
render() {
return(
...
<DragDropContext onDragEnd={this.onDragEnd}>
...
</DragDropContext>
)
}
次は、Droppable
な領域を<Droppable></Droppable>
で囲っていきます。Droppable
な領域は今回は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>
)
}
}
このDroppable
には、一意のID
が必要です。
これはなぜかというと、異なる列での要素の行き来があった場合に、その列はどの列か?を認識するために必要だからです。
例えばTodo
のIDがcolumn-1
として、done
のIDがcolumn-2
とします。
ゴミ出しをするというTodoを登録すると、現在ゴミ出しはTodoにあることになるため、IDで表すと「column-1
の列にゴミ出しのタスクがある」と言えます。
そして、ゴミ出しを完了し、Todo
からdone
へタスクをdndしたとします。それをIDで表すと
「column-1
の列にあったゴミ出しのタスクがcolumn-2
へ移動した」
ということになります。
もしIDを持っていたとすればそれはどこに移動したがが重複することになり、結果どこに移動したかがわからなくなります。そのため、一意のID
を設定する必要があります。
今回の場合には、inisial-data.js
内でcolumns
でcolumn-1
という値を設定しているため、これを利用します。
dorpppableId
にIDを設定します。
<Droppable droppableId={this.props.column.id}>
</Droppable>
また、provided
引数を持つ関数でラップしてあげる必要があります。
このprovided
に含まれる値を元に、どのアイテムがどの位置に移動されたかを追跡することができるようになります。
(この処理がどうなっているのかを詳しく説明できなくてすみません、勉強します。refについてはこちら)
そして、もう一つ大切なplaceholder
を追加します。
これはドラッグ中にドロップ可能なエリアを増やすために必要になります。プレースホルダーは、ドロップ可能として指定したコンポーネントの子として追加する必要があります。
また、tasksをmapで回しており、このtaskはどのindexかを判定するために必要なのでindexを第二引数に設定し、子コンポーネントに渡します。
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
コンポーネントをドラッグ可能にするための処理を書いていきます。
まずはインポート、そして<Draggable></Draggable>
で要素を囲み、draggableId
を設定していきます。
さらに、Column
コンポーネントと同じようにprovided
引数を持つ関数でラップして、追跡できるようにします。
今回新しく{...provided.dragHandleProps}
という一文が追加されています。これを追加することによって、draggableなコンポーネントをドラッグすることができるようになります。
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>
)
}
}
ここまで記述できれば、要素をdndできるようになっていると思います。
しかし、問題点があります。それはdndした要素が元の位置に戻ってしまう
ことです。
次はその問題を解消していきましょう。
onDragEndコールバックを使用してリストの並び替えの永続化
それではindex.js
に戻って、onDragEndコールバックの処理を記述していきます。
resultについて
その前に、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
、つまりドラッグし終わった後の結果の情報が詰まっています。
重要なプロパティだけ解説します。
・draggableId
: ドラッグしている要素のid
・sourceのdroppableId
: ドロップされる前のカラムのid
・sourceのindex
: ドロップされる前のindex
・destinationのdroppableId
: ドロップされた後のカラムのid
・destinarionのindex
: ドロップされた後のindex
これらを用いて、インタラクションの永続化を実現します。
イメージとしては、ドラッグされている要素がどのカラムのどのindexにあって、それがどのカラムのどのindexにドロップされたかを取得します。そして、stateを変更することでReactがstateの変更を検知し、DOMが上書きされるといった感じです。
ではonDragEndに処理を書いていきましょう。
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)
}
ここまでかければ、アプリを確認してください。タスクの移動が永続化されたことがわかります。
これでいったん1列のみのdnd可能なtodoアプリの制作ができました。
次からは、よりdndを美しく見せるための工夫をしてみましょう。
react-beautiful-dndのsnapshotを用いてドラッグ中のスタイルを変更する
Draggable
とDroppable
には、それぞれsnapshotというオブジェクトが存在します。snapshotはドラッグ中のスタイルを変更するために使用できる便利なプロパティが設定されています。
snapshotのプロパティの解説
では、プロパティについてみていきましょう。
// Draggable
const draggableSnapshot = {
isDragging: boolean,
draggingOver: 'column-1'
}
// Droppable
const droppableSnapshot = {
isDraggingOver: boolean,
draggingOverWith: 'task-1'
}
それぞれ、Draggable
なコンポーネントと、Droppable
なコンポーネントのsnapshotに設定されているものです。
まず、draggableSnapshot
のisDragging
ですが、これはBooleanを値にとります。これはドラッグ可能なコンポーネントがドラッグされている時にtrueになります。今回のアプリでいうと、taskを持って移動するタイミングを示しています。
次に、draggableSnapshot
のdraggingOver
ですが、これはドラッグ可能なコンポーネントが、ドロップ可能なコンポーネント上にある時に、ドロップ可能なコンポーネントのid
が値になります。
また、ドラッグ可能なコンポーネントが、ドロップ不可のエリア上にあるときにはnull
が入ります。
droppableSnapshot
のisDraggingOver
ですが、これはドラッグ可能なコンポーネントが、ドロップ可能なコンポーネント上にある時にtrue
となります。
また、droppableSnapshot
のdraggingOverWith
は、ドロップ可能なコンポーネント上にあるドラッグ可能なコンポーネントのid
が入ります。
snapshotを使ってみる
使い方は簡単です。
draggableなコンポーネントに対する処理
まず、ドラッグを可能にしたコンポーネントへ移動します。つまりTask.jsx
に移動します。
そして、{(provided, snapshot) => (...)}
のように、provided
の後に続けてsnapshot
を追加します。
次に、ドラッグ可能なコンポーネントに対してisDragging={snapshot.isDragging}
を追加します。
最後にstyled-component
内で、条件分岐で色を変化させてあげればOKです✌️
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>
)
}
}
ブラウザで確認すると、ドラッグ時に背景色がlightgreenになって、置いたときにはwhiteになっているかと思います。
こんな感じで、draggable
コンポーネントの状態を取得し、スタイルを変更させることができるようになりました。
droppableなコンポーネントに対する処理
では次はdroppable
なコンポーネント、つまり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>
)
}
}
この状態で確認すると、ドラッグしているときには背景色がskyblueに変更され、置いた時にはwhiteになっています。
列間でタスクの移動ができるようにする
それでは、列を追加してみましょう。
列は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
ここでは「進行中」「完了」の2つの列を追加しています。idはそれぞれ一意になるようにし、taskIdsは空の状態にしておきます。
そして、columnOrder
で順番を指定しています。
すると、ブラウザには「Todo」「Progress」「Done」の3つの列ができます。
カラムのスタイルを整える
このままでは3つの列が縦に並んでいるので、横並びにしておきましょう。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;
`
そして、現段階でタスクを移動させようとしても、背景色が全体まで伸びないと思います。これはProgress
カラムと、Done
カラムに高さがないためです。
これを解消するためにスタイルを当てます。
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
カラムにTodoを移動させても正常に動かないと思います。
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-componrntsの書き方も学びになりました。やはり実際にプロが書いているコードを見るのも大切ですね。
あとは初めて英語で解説されている講座をみたのですが、何を言っているのかがわからなくてもコードを見れば何がしたいのかはわかりますし、翻訳していけばなんとかなることが分かりました。あとは、console.log
をめちゃくちゃ実行して何が入っているのかを逐一確認すること。これ一番大事だなと思いました。
今後の展開
・inputエリアを用意して自分で値を追加できるようにする
・firebaseと連携して、TODOなどを保持できるようにしたい
・classコンポーネントを使った書き方だったので、関数コンポーネントに書き直したり、React hooksを使って書き直したい
・UIが簡素なので、Material UIなども使っていきたい
・実際に職場で使ってもらい、改善・修正していきたい
・CRUDのCしかできていないので、あとの機能も追加していきたい
宣伝
ほぞぼそと更新しています。
ブログ → shoublog
Twitter → syoyamamoto
現在もりけん塾で学習中です。私はここでJavaScriptの基礎を学びました。固定Twiite必見。
もりけん先生 → @terrace_tech
もりけん先生のブログ → 無骨日記
また、もう一つUdemyで有名なはむさんのコミュニティーでも学習しています。
こちらは現役エンジニアの方がごろごろいらっしゃるので、いつもお世話になっています。
はむさん → @diveintohacking
Udemy → はむさん リーマンショックのリストラから這い上がったウェブ系エンジニア
コミュニティ → はむこみ プロフィールからどうぞ