【React】To doアプリを作ってみた!react-beautiful-dndでドラッグ&ドロップしてタスクの状態を変更

React
スポンサーリンク

Reactを業務で使うので、とりあえずTo doアプリを作ってみました。

タスクを追加して、タスクの状態をドラッグ&ドロップで変更できる感じにしてみました。

↓まずは触ってみてください。

See the Pen react to do list by amateur-engineer (@amaeng) on CodePen.

この記事では、このTo doアプリの作り方を説明します。

ドラッグ&ドロップできるTo doアプリの作り方

プロジェクトの作成

npx create-react-app my-app
cd my-app
npm start

create-react-appで新しいシングルページアプリケーションを作成します。
my-appのところは、適当なアプリケーション名に変更してください。

npm startを実行すると、ブラウザに自動でアプリの画面が表示されます。

いっぱいファイルがありますが、ポイントとなるものは下記の5つだと思います。

.
├── public
│   └──index.html
└── src
    ├──App.css
    ├──App.js
    ├──index.css
    └──index.js

index.htmlのルートDOM ノード(<div id=”root”></div>)に、index.jsでReact要素をレンダーしています。

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';

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

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

<App />をレンダーするように実装されていますので、App.jsを任意の実装に書き換えればOKです。

使用するパッケージのインストール

今回は、ドラッグ&ドロップを行うのにreact-beautiful-dndを使用します。

npm install react-beautiful-dnd

React hooksも使用するので、App.jsに次の二つをimportします。

import React, { useState } from 'react';
import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd';

To do listの実装部分

実装の全体像は以下の通りです。説明ついては後で記載しますが、細かい実装については省きます。

import React, { useState } from 'react';
import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd';
import logo from './logo.svg';
import './App.css';

//To do listのタスクの状態
const listName = {
  list1: 'todo',
  list2: 'progress',
  list3: 'done'
};

//To do list内のタスクの順番を変更
const reorder = (list, startIndex, endIndex) => {
  const result = Array.from(list);
  const [removed] = result.splice(startIndex, 1);
  result.splice(endIndex, 0, removed);
  return result;
};

//To do list内のタスクを削除
const deleteItem = (list, index) => {
  const result = Array.from(list);
  result.splice(index, 1);
  return result;
};

//To do listのタスクの状態を変更
const move = (source, destination, droppableSource, droppableDestination) => {
  const sourceClone = Array.from(source);
  const destClone = Array.from(destination);
  const [removed] = sourceClone.splice(droppableSource.index, 1);
  destClone.splice(droppableDestination.index, 0, removed);
  const result = {};
  result[droppableSource.droppableId] = sourceClone;
  result[droppableDestination.droppableId] = destClone;
  return result;
};

//To do list内のアイテム(タスク)のcss
const getItemStyle = draggableStyle => ({
  displey: 'flex',
  padding: '1rem',
  marginBottom: '0.5rem',
  background: '#fff8e8',
  borderLeft: 'solid 0.5rem #ffc06e',
  color: '#282c34',

  ...draggableStyle
});

//To do listのcss
const getListStyle = isDraggingOver => ({
  padding: '1rem',
  margin: '1rem',
  background: 'white',
  minWidth: '200px',
  height: '70vh',
  border: isDraggingOver ? 'solid 5px lightgray' : 'solid 5px white',
  borderRadius: '0.5rem',
  textAlign: 'left',
});

function List(props) {
  const listTitle = {
    list1: 'To do',
    list2: 'In progress',
    list3: 'Done'
  };

  return (
    <div className="To-do-list">
      <Droppable droppableId={props.id}>
        {(provided, snapshot) => (
          <div
            {...provided.droppableProps}
            ref={provided.innerRef}
            style={getListStyle(snapshot.isDraggingOver)}
          >
            <h2>{listTitle[props.id]}</h2>
            {props.list.map((item, index) => (
              <Draggable key={item.id} draggableId={item.id} index={index}>
                {(provided) => (
                  <div
                    ref={provided.innerRef}
                    {...provided.draggableProps}
                    {...provided.dragHandleProps}
                    style={getItemStyle(provided.draggableProps.style)}
                  >
                    <input
                      type="text"
                      className="Item-form"
                      placeholder="Please enter your task"
                      value={item.text}
                      onChange={e => props.onUpdateItems(props.id, index, e)}
                    />
                    <button className="Delete-item-btn" onClick={() => props.onDeleteItemForList(props.id, index)}></button>
                  </div>
                )}
              </Draggable>
            ))}
            {provided.placeholder}
            <button className="Add-item-btn" onClick={() => props.onAddItems(props.id)}></button>
          </div>
        )}
      </Droppable>
    </div>
  );
}

function ToDoListContainer() {
  const [todo, setTodoList] = useState([
    {
      id: 'item-1',
      text: ''
    }
  ]);
  const [progress, setProgressList] = useState([]);
  const [done, setDoneList] = useState([]);
  const [itemCount, setItemCount] = useState(1);

  const getList = id => {
    if (listName[id] === 'todo') {
      return todo;
    } else if (listName[id] === 'progress') {
      return progress;
    } else if (listName[id] === 'done') {
      return done;
    }
  }

  const setItemInList = (id, list) => {
    if (listName[id] === 'todo') {
      setTodoList(list);
    } else if (listName[id] === 'progress') {
      setProgressList(list);
    } else if (listName[id] === 'done') {
      setDoneList(list);
    }
  }

  const onDragEnd = result => {
    const { source, destination } = result;
    if (!result.destination) {
      return;
    }
    if (source.droppableId === destination.droppableId) {
      const update = reorder(
        getList(source.droppableId),
        source.index,
        destination.index
      );
      setItemInList(source.droppableId, update);
    } else {
      const result = move(
        getList(source.droppableId),
        getList(destination.droppableId),
        source,
        destination
      );
      setItemInList(source.droppableId, result[source.droppableId]);
      setItemInList(destination.droppableId, result[destination.droppableId]);
    }
  }

  const addItems = id => {
    setItemInList(
      id,
      getList(id).concat(
        {
          id: `item-${itemCount + 1}`,
          text: ''
        }
      )
    );
    setItemCount(itemCount + 1);
  }

  const updateItems = (id, idx, e) => {
    const list_copy = getList(id).slice();
    list_copy[idx].text = e.target.value;
    setItemInList(id, list_copy);
  }

  const deleteItemForList = (id, idx) => {
    const removed = deleteItem(getList(id),idx);
    setItemInList(id, removed);
  }

  return (
    <DragDropContext onDragEnd={onDragEnd}>
      <div className="To-do-list-container">
        {Object.keys(listName).map(key => 
          <List 
            key={key} 
            id={key} 
            list={getList(key)} 
            onAddItems={addItems} 
            onUpdateItems={updateItems}
            onDeleteItemForList={deleteItemForList}
          />
        )}
      </div>
    </DragDropContext>
  );
}

function App() {
  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <h1>
          To do list
        </h1>
      </header>
      <ToDoListContainer />
    </div>
  );
}

export default App;
.App {
  background-color: whitesmoke;
  height: 100vh;
  text-align: center;
}

.App-logo {
  height: 10vmin;
  pointer-events: none;
}

@media (prefers-reduced-motion: no-preference) {
  .App-logo {
    animation: App-logo-spin infinite 20s linear;
  }
}

.App-header {
  min-height: 15vh;
  display: flex;
  flex-direction: row;
  align-items: center;
  justify-content: center;
  font-size: calc(10px + 2vmin);
  color: #282c34;
}

@keyframes App-logo-spin {
  from {
    transform: rotate(0deg);
  }
  to {
    transform: rotate(360deg);
  }
}

.To-do-list-container {
  display: flex;
  flex-wrap: wrap;
}

.To-do-list {
  flex: 1;
}

.Add-item-btn {
  padding: 1rem;
  margin: 0;
  width: 100%;
  text-align: left;
  background: white;
  border-left: solid 10px white;
  border-right: none;
  border-top: none;
  border-bottom: none;
}

.Add-item-btn:before {
  font-weight: bold;
  font-size: 1rem;
  line-height: 1rem;
  content: "+";
  opacity: 0.5;
}

.Add-item-btn:hover {
  background: #fff8e8;
  border-left: solid 10px #ffc06e;
}

.Item-form {
  padding: 0;
  margin: 0;
  resize: none;
  border: none;
  font-size: 1em;
  width: 95%;
  height: 100%;
  background: #fff8e8;
}

.Item-form:focus {
  outline: 0;
}

.Delete-item-btn {
  padding: 0;
  width: 5%;
  background: #fff8e8;
  border: none;
  transform: rotate(45deg);
}

.Delete-item-btn:before {
  font-weight: bold;
  font-size: 1rem;
  line-height: 1rem;
  content: "+";
  opacity: 0.05;
}

.Delete-item-btn:hover:before {
  opacity: 1;
}

ヘッダー部分

ヘッダーは、既存の実装をいじってReactのロゴを再利用します。

function App() {
  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <h1>
          To do list
        </h1>
      </header>
      <ToDoListContainer />
    </div>
  );
}

<ToDoListContainer />にTo do listを実装します。

ドラッグ&ドロップ部分のイメージ

react-beautiful-dndを使用してドラッグ&ドロップができるように実装しています。

<DragDropContext>で囲んで、ドロップできる範囲<Droppable>とドラッグ可能な要素<Draggable>を定義します。

<DragDropContext onDragEnd={onDragEnd}>
  <Droppable droppableId={props.id}>
    <Draggable key={item.id} draggableId={item.id} index={index}>

    </Draggable>
  </Droppable>
</DragDropContext>

最初のステップとしては、こちらのページにあるBasic samplesをいくつか見てみることをお勧めします。

To do listのContainer

function ToDoListContainerでは、idとtextを持つ3つのリストを定義しています。

function ToDoListContainer() {
  const [todo, setTodoList] = useState([
    {
      id: 'item-1',
      text: ''
    }
  ]);
  const [progress, setProgressList] = useState([]);
  const [done, setDoneList] = useState([]);

それぞれをtodo、progress、doneとしてタスクの状態を表します。

そして、次のJSKを返します。

function ToDoListContainer() {

  ・・・

  return (
    <DragDropContext onDragEnd={onDragEnd}>
      <div className="To-do-list-container">
        {Object.keys(listName).map(key => 
          <List 
            key={key} 
            id={key} 
            list={getList(key)} 
            onAddItems={addItems} 
            onUpdateItems={updateItems}
            onDeleteItemForList={deleteItemForList}
          />
        )}
      </div>
    </DragDropContext>
  );
}

定義しているイベント処理は次の通りです。

イベント処理
  • <DragDropContext>のイベント処理
    • onDragEnd={onDragEnd}
      →ドラッグ後のイベント処理。タスクの状態や順番を変更する。
  • <List />のイベント処理
    • onAddItems={addItems}
      →アイテム(タスク)を追加する。
    • onUpdateItems={updateItems}
      →アイテム(タスク)のテキストの内容を変更する。
    • onDeleteItemForList={deleteItemForList}
      →アイテム(タスク)を削除する。

<List />のイベント処理はfunction Listに渡しています。

To do listの中身

function Listでタスクのリストを並べていきます。

JSXは、ドロップするエリアとドラッグするアイテムの2つに分けられます。

ドロップするエリア

<Droppable>でTo do listの枠とタイトル、タスク追加ボタンを設計

<Droppable droppableId={props.id}>
  {(provided, snapshot) => (
    <div
      {...provided.droppableProps}
      ref={provided.innerRef}
      style={getListStyle(snapshot.isDraggingOver)}
    >
      <h2>{listTitle[props.id]}</h2>
      {// ここにドラッグ可能なアイテムを配置 }
      {provided.placeholder}
      <button className="Add-item-btn" onClick={() => props.onAddItems(props.id)}></button>
    </div>
  )}
</Droppable>
ドラッグするアイテム

<Draggable>内の<input>でテキスト入力可能にして、タスクの削除ボタンも配置。


{props.list.map((item, index) => (
  <Draggable key={item.id} draggableId={item.id} index={index}>
    {(provided) => (
      <div
        ref={provided.innerRef}
        {...provided.draggableProps}
        {...provided.dragHandleProps}
        style={getItemStyle(provided.draggableProps.style)}
      >
        <input
          type="text"
          className="Item-form"
          placeholder="Please enter your task"
          value={item.text}
          onChange={e => props.onUpdateItems(props.id, index, e)}
        />
        <button className="Delete-item-btn" onClick={() => props.onDeleteItemForList(props.id, index)}></button>
      </div>
    )}
  </Draggable>
))}

ドラッグ&ドロップの状態でstyleを変更

To do listの枠線は、ドラッグ中に表示されるようにしました。

//To do listのcss
const getListStyle = isDraggingOver => ({
  padding: '1rem',
  margin: '1rem',
  background: 'white',
  minWidth: '200px',
  height: '70vh',
  border: isDraggingOver ? 'solid 5px lightgray' : 'solid 5px white',
  borderRadius: '0.5rem',
  textAlign: 'left',
});

isDraggingOverにてborderのstyleを変更するようにしています。

まとめ

To doアプリの作り方について説明しました。
Reactは初めてなので、実装でおかしなところがあるかもしれません。
そこは流していただければと思います。

Reactの学習では、公式ドキュメントのHello Worldから順番に学びました。
こちらを一通り学習すると、ある程度何か作れるようになると思います。
もし初学者の方がいらっしゃったら、やってみることをお勧めします。

コメント

タイトルとURLをコピーしました