30
32

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

【React】ReactでTrello風のカンバンアプリを作る手順【react-beautiful-dnd】

Last updated at Posted at 2021-03-16

はじめに

今回、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ファイル内も整理しましょう。

index.js
import React from 'react';
import ReactDOM from 'react-dom';

ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById('root')
);

そして、きちんと動作するかを確認するために画面にhello worldを出力してみましょう。

index.js
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
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のタイトルを出力するコードを記述します。

index.js
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です。
ではただしく動作していることがわかったので、次はタスクを列挙していきます。

また、ここからJavaScriptReactについての記述については詳しく解説しませんが、適宜コメントをいれるなどして解説していこうと思います。(リンクなども貼っていきます)
もし書いてある処理がわからないなどであれば、JavaScriptの基礎知識Reactの基礎知識が不足しているので、もう一度基礎からやり直してから戻ってくるか、調べながら付いてきていただければと思います。

ではindex.jsに戻って、Columnコンポーネントをレンダリングするための処理を記述します。

index.js
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 // 新規作成
Column.jsx
import React from 'react';

export default class Column extends React.Component {
  render() {
    return (
      this.props.column.title
    )
  }
}

そして、index.jsColumnコンポーネントをimportしましょう。

index.js
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をインポートしていきます。

index.js
import React from 'react';
import ReactDOM from 'react-dom';
import '@atlaskit/css-reset' // 追加
import initialData from './initial-data'
import Column from './Column'

するとスタイルが変わったかと思います。

次はstyled-componentsを使ってスタイルを当てていきます。
インポートして、スタイルを当てたいコンポーネントを定義します。

Column.jsx
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
    )
  }
}

そしてそれぞれのコンポーネントに付与したいスタイルを書いていきます。

Column.jsx
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内に記述します。

Column.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を表示させていく処理を記述します。

Column.jsx
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 // 新規追加
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です✌️
スクリーンショット 2021-03-15 20.40.44.png

react-beautiful-dndでリストを並び替える

react-beautiful-dndの概要

まず手を動かす前にreact-beautiful-dndがどのようなコンポーネントで構成されているかを見ていきましょう。
react-beautiful-dnd.gif
こちらは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になります。

index.js
import { DragDropContext } from "react-beautiful-dnd";

そして、<DragDropContext>でdndしたい領域を囲みましょう。

index.js

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です。

index.js

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内で定義したメソッドを渡してあげます。またここに戻ってくるので、次に進みます。

index.js
const onDragEnd = result => {}

render() {
  return(
    ...
    <DragDropContext onDragEnd={this.onDragEnd}>
    ...
    </DragDropContext>
  )
}

次は、Droppableな領域を<Droppable></Droppable>で囲っていきます。Droppableな領域は今回はColumnコンポーネントです。

Column.jsx
import { Droppable } from "react-beautiful-dnd";
Column.jsx
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内でcolumnscolumn-1という値を設定しているため、これを利用します。
dorpppableIdにIDを設定します。

Column.jsx
<Droppable droppableId={this.props.column.id}>
</Droppable>

また、provided引数を持つ関数でラップしてあげる必要があります。
このprovidedに含まれる値を元に、どのアイテムがどの位置に移動されたかを追跡することができるようになります。
(この処理がどうなっているのかを詳しく説明できなくてすみません、勉強します。refについてはこちら)

そして、もう一つ大切なplaceholderを追加します。
これはドラッグ中にドロップ可能なエリアを増やすために必要になります。プレースホルダーは、ドロップ可能として指定したコンポーネントの子として追加する必要があります。

また、tasksをmapで回しており、このtaskはどのindexかを判定するために必要なのでindexを第二引数に設定し、子コンポーネントに渡します。

Column.jsx
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なコンポーネントをドラッグすることができるようになります。

Task.jsx
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)などで中身を自分で確認するとより理解が深まります。(自分がそうでした)

example-result.js
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の処理

処理を書いていきます。都度コメントを書いているので参考にしながらコードを書いてみてください。

index.js
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を用いてドラッグ中のスタイルを変更する

DraggableDroppableには、それぞれsnapshotというオブジェクトが存在します。snapshotはドラッグ中のスタイルを変更するために使用できる便利なプロパティが設定されています。

snapshotのプロパティの解説

では、プロパティについてみていきましょう。

examle-snapshot.js
// Draggable
const draggableSnapshot = {
  isDragging: boolean,
  draggingOver: 'column-1'  
}

// Droppable
const droppableSnapshot = {
  isDraggingOver: boolean,
  draggingOverWith: 'task-1'
}

それぞれ、Draggableなコンポーネントと、Droppableなコンポーネントのsnapshotに設定されているものです。

まず、draggableSnapshotisDraggingですが、これはBooleanを値にとります。これはドラッグ可能なコンポーネントがドラッグされている時にtrueになります。今回のアプリでいうと、taskを持って移動するタイミングを示しています。
次に、draggableSnapshotdraggingOverですが、これはドラッグ可能なコンポーネントが、ドロップ可能なコンポーネント上にある時に、ドロップ可能なコンポーネントのidが値になります。
また、ドラッグ可能なコンポーネントが、ドロップ不可のエリア上にあるときにはnullが入ります。

droppableSnapshotisDraggingOverですが、これはドラッグ可能なコンポーネントが、ドロップ可能なコンポーネント上にある時にtrueとなります。
また、droppableSnapshotdraggingOverWithは、ドロップ可能なコンポーネント上にあるドラッグ可能なコンポーネントのidが入ります。

snapshotを使ってみる

使い方は簡単です。

draggableなコンポーネントに対する処理

まず、ドラッグを可能にしたコンポーネントへ移動します。つまりTask.jsxに移動します。
そして、{(provided, snapshot) => (...)}のように、providedの後に続けてsnapshotを追加します。

次に、ドラッグ可能なコンポーネントに対してisDragging={snapshot.isDragging}を追加します。

最後にstyled-component内で、条件分岐で色を変化させてあげればOKです✌️

Task.jsx
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に移動します。
先ほどやった手順とさほど変わらないので、解説は飛ばします。

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で管理していましたので、移動します。

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に移動します。

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>
    )
  }

これで横並びになりましたが、横幅を指定していないので変更します。

Column.jsx
const Container = styled.div`
  margin: 8px;
  border: 1px solid lightgray;
  border-radius: 2px;
  width: 200px;
`

そして、現段階でタスクを移動させようとしても、背景色が全体まで伸びないと思います。これはProgressカラムと、Doneカラムに高さがないためです。
これを解消するためにスタイルを当てます。

Column.jsx
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の処理を修正する必要があります。

index.js
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)
  }

まずは開始時のカラムと終了時のカラムを取得する処理を記述し、開始時と終了時が同じカラムである場合には今まで通りの処理を実行するといった風に書き換えます。

index.js
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  // ここを通ったら処理を終わらせる
  } 
}

同列間では今まで通りタスクの移動が可能なハズです。
最後に異なる列での移動を可能にする処理を書きます。ここの処理内容はすでに書いてある処理に近いのでわかりやすいかと思います。

index.js

// 開始カラムと終了カラムが違い場合の処理
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の全体だけ載せておきます。

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 → はむさん リーマンショックのリストラから這い上がったウェブ系エンジニア
コミュニティ → はむこみ プロフィールからどうぞ

30
32
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
30
32

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?