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>
);
}
定義しているイベント処理は次の通りです。
<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から順番に学びました。
こちらを一通り学習すると、ある程度何か作れるようになると思います。
もし初学者の方がいらっしゃったら、やってみることをお勧めします。
コメント