Reactで開発をしていて、テーブルの行を自由に並び替えたいって思うことありますよね。
ということで、ドラッグ&ドロップで行を入れ替えられるテーブルを作成してみました。
See the Pen react-dnd-table by amateur-engineer (@amaeng) on CodePen.
1から実装するのは面倒だったので、
- テーブルはMaterial-UI
- ドラッグ&ドロップはreact-beautiful-dnd
を使って実装しています。
それでは、作り方を紹介していきます。
ドラッグ&ドロップできるテーブルの作り方
事前準備
まずは下記の環境を整えます。
- React + TypeScript
- Material-UI(UIライブラリ)
- react-beautiful-dnd(ドラッグ&ドロップ部分)
React + TypescriptとMaterial-UIについては別記事に書きましたので、そちらを見てください。
react-beautiful-dndをTypeScriptで使用するには、npm installでreact-beautiful-dndと@types/react-beautiful-dndをインストールします。
npm install react-beautiful-dnd
npm install @types/react-beautiful-dnd
@types/から始まるパッケージは、JavaScriptのライブラリに型定義を付与するファイルが入っているらしいです。
通常のテーブルを作成
import * as React from 'react';
import Table from '@mui/material/Table';
import TableBody from '@mui/material/TableBody';
import TableCell from '@mui/material/TableCell';
import TableContainer from '@mui/material/TableContainer';
import TableHead from '@mui/material/TableHead';
import TableRow from '@mui/material/TableRow';
import Paper from '@mui/material/Paper';
const rows = [
{name: 'item1', index: 0, order: 0},
{name: 'item2', index: 1, order: 1},
{name: 'item3', index: 2, order: 2},
{name: 'item4', index: 3, order: 3},
{name: 'item5', index: 4, order: 4},
];
export default function BasicTable() {
return (
<TableContainer component={Paper}>
<Table>
<TableHead>
<TableRow>
<TableCell>Name</TableCell>
<TableCell>Index</TableCell>
<TableCell>Order</TableCell>
</TableRow>
</TableHead>
<TableBody>
{rows.map((row) => (
<TableRow
key={row.name}
>
<TableCell >{row.name}</TableCell>
<TableCell>{row.index}</TableCell>
<TableCell>{row.order}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
);
}
最初に基盤となるテーブルを作成します。
Material-UIのTableを使用して、できるだけ簡素に作りました。
この後並び替えで使用するorderとindexは0オリジンです。
ドラッグ&ドロップできるように修正
import * as React from "react";
import {
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Paper,
} from "@mui/material";
import { DragDropContext, Droppable, Draggable, DropResult } from "react-beautiful-dnd";
export default function DragAndDropTable() {
const [rows, setRows] = React.useState([
{ name: "item1", index: 0, order: 0 },
{ name: "item2", index: 1, order: 1 },
{ name: "item3", index: 2, order: 2 },
{ name: "item4", index: 3, order: 3 },
{ name: "item5", index: 4, order: 4 },
]);
const reorder = (startIndex: number, endIndex: number) => {
const result = Array.from(rows);
const [removed] = result.splice(startIndex, 1);
result.splice(endIndex, 0, removed);
result.map((row, index) => (row.order = index));
return result;
};
const onDragEnd = (result: DropResult) => {
const { source, destination } = result;
if (!destination) {
return;
}
const update = reorder(source.index, destination.index);
setRows(update);
};
return (
<TableContainer component={Paper}>
<Table>
<TableHead>
<TableRow>
<TableCell>Name</TableCell>
<TableCell>Index</TableCell>
<TableCell>Order</TableCell>
</TableRow>
</TableHead>
<DragDropContext onDragEnd={onDragEnd}>
<Droppable droppableId={"dndTableBody"}>
{(provided) => (
<TableBody ref={provided.innerRef} {...provided.droppableProps}>
{rows.map((row, index) => (
<Draggable
draggableId={row.name}
index={index}
key={row.name}
>
{(provided) => (
<TableRow
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
>
<TableCell style={{minWidth: `${Math.floor(100/3)}vw`}}>{row.name}</TableCell>
<TableCell style={{minWidth: `${Math.floor(100/3)}vw`}}>{row.index}</TableCell>
<TableCell style={{minWidth: `${Math.floor(100/3)}vw`}}>{row.order}</TableCell>
</TableRow>
)}
</Draggable>
))}
{provided.placeholder}
</TableBody>
)}
</Droppable>
</DragDropContext>
</Table>
</TableContainer>
);
}
役割ごとに説明していきます。
全体はDraoDropContextで囲む
<DragDropContext onDragEnd={onDragEnd}>
{/** ドラッグ&ドロップしたいエリア */}
</DragDropContext>
ドラッグ&ドロップしたいエリア全体をDragdropContextで囲みます。
ドロップエリアはDroppableで囲む
<Droppable droppableId={"dndTableBody"}>
{(provided) => (
<TableBody ref={provided.innerRef} {...provided.droppableProps}>
{/** ドラッグするアイテム */}
{provided.placeholder}
</TableBody>
)}
</Droppable>
ドラッグしたコンポーネントをドロップするエリアは、Droppableで囲みます。
TableBody内をドロップ可能エリアに指定しています。
ドラッグするコンポーネントはDraggableで囲む
{rows.map((row, index) => (
<Draggable
draggableId={row.name}
index={index}
key={row.name}
>
{(provided) => (
<TableRow
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
>
<TableCell style={{minWidth: `${Math.floor(100/3)}vw`}}>{row.name}</TableCell>
<TableCell style={{minWidth: `${Math.floor(100/3)}vw`}}>{row.index}</TableCell>
<TableCell style={{minWidth: `${Math.floor(100/3)}vw`}}>{row.order}</TableCell>
</TableRow>
)}
</Draggable>
))}
ドラッグしたいコンポーネントは一つずつDraggableで囲みます。
Material-UIのテーブル行をドラッグしたときにデザインが崩れたため、minWidthを均等になるように指定しています。
ドラッグ後のアクションはonDragEndで指定
<DragDropContext onDragEnd={onDragEnd}>
DragDropContextのonDragEndで、ドラッグ後のアクションを指定できます。
テーブルの行を入れ替えたい場合、ここで入れ替えを行います。
const onDragEnd = (result: DropResult) => {
const { source, destination } = result;
if (!destination) {
return;
}
const update = reorder(source.index, destination.index);
setRows(update);
};
onDragEndの処理内容は、ざっくり説明すると次のような感じです。
- ドロップ領域(destination)を外れた場合
→何しない - ドロップ領域(destination)内
→行をドラッグ前(source.index)からドラッグ後(destination.index)に移動
const reorder = (startIndex: number, endIndex: number) => {
const result = Array.from(rows);
const [removed] = result.splice(startIndex, 1);
result.splice(endIndex, 0, removed);
result.map((row, index) => (row.order = index));
return result;
};
並び替えは、Array.prototype.splice()で要素を取り除いてから指定箇所に追加します。
その後、並び順でorderを採番しています。
まとめ:深く考えなければ簡単にできる
react-beautiful-dndはよくわからない箇所が多いので、あまり考えずにコピペで動かしてみてください。コピペで動けば、あとは簡単です。
コメント