巷で難しい難しいと言われていた react-dnd でドラッグアンドドロップを実装してみたところ、案外楽だったのでまとめる。
用語整理
-
DragSource
: マウスで掴んでドラッグする対象 -
DropTarget
: ドロップ対象 -
monitor
: drop/hover時に今現在のイベント対象の状態を取り出せるもの
使い方
-
@DragDropContext
を ドラッグアンドドロップしたい領域のコンポーネントに注入する -
@DropTarget
を落としたい先のコンポーネントに注入する -
@DragSource
をドラッグさせたいコンポーネントに注入する
たとえばドラッグアンドドロップでリストの要素を入れ替えたい場合、 リスト全体が DragDropContext
で、 リスト要素が DragSource
かつ DropTarget
になる。
実装例
並び替えられるリスト要素の実装
@DropTarget("item", {
// drop 時のコールバック
drop(dropProps, monitor, dropComponent) {
const dragProps = monitor.getItem(); // DragSource の props が取り出せる
if (dropProps.id !== dragProps.id) {
dragProps.onDrop(dragProps.id, dropProps.id);
}
}
}, connect => {
return {
connectDropTarget: connect.dropTarget()
};
})
@DragSource("item", {
beginDrag(props) {
return props;
}
}, (connect, monitor) => {
return {
connectDragSource: connect.dragSource(),
isDragging: monitor.isDragging()
};
})
class DragItem extends Component {
render() {
return this.props.connectDragSource(this.props.connectDropTarget(
<div>
{this.props.children}
</div>
));
}
}
リストの実装
@DragDropContext(ReactDnDHTML5Backend)
export default class SortableList extends Component {
constructor(props) {
super(props);
this.state = {
items: [
{id: 1, name: "aaa"},
{id: 2, name: "bbb"},
{id: 3, name: "ccc"}
];
}
}
render() {
return (
<div>
{
this.state.items.map(item => {
return <DragItem
key={item.id}
id={item.id}
onDrop={
(toId, fromId) => {
// ここで入れ替える処理をする
const items = this.state.items.slice();
const toIndex = items.findIndex(i => i.id === toId);
const fromIndex = items.findIndex(i => i.id === fromId);
const toItem = items[toIndex];
const fromItem = items[fromIndex];
items[toIndex] = fromItem;
items[fromIndex] = toItem;
this.setState({items})
}
}
>
{item.name}
</DragItem>
})
}
</div>
);
}
}
この実装例だと onDrop コールバックを与えて、from, to を取得し、state.items の中身を入れ替える。flux だったら store に向かって投げるだろう。
追記: 久しぶりに見直したらまだ便利だったので、TypeScript版も書いておく。デコレータは使わない。正直、型はうまくつかない。
import React from "react"
import { DragDropContext, DragSource, DropTarget } from "react-dnd"
import ReactDnDHTML5Backend from "react-dnd-html5-backend"
const DND_GROUP = "sortable"
type Item = { id: string; name: string }
const SortableList = DragDropContext(ReactDnDHTML5Backend)(
class SortableListImpl extends React.Component<any, { items: Item[] }> {
constructor(props: any) {
super(props)
this.state = {
items: [
{ id: "1", name: "aaa" },
{ id: "2", name: "bbb" },
{ id: "3", name: "ccc" }
]
}
}
render() {
return (
<div>
{this.state.items.map(item => {
return (
<DraggableItem
key={item.id}
id={item.id}
onDrop={(toId: string, fromId: string) => {
// ここで入れ替える処理をする
const items = this.state.items.slice()
const toIndex = items.findIndex(i => i.id === toId)
const fromIndex = items.findIndex(i => i.id === fromId)
const toItem = items[toIndex]
const fromItem = items[fromIndex]
items[toIndex] = fromItem
items[fromIndex] = toItem
this.setState({ items })
}}
>
{item.name}
</DraggableItem>
)
})}
</div>
)
}
}
)
const DraggableItem: React.ComponentType<{ id: string; onDrop: any }> = compose(
DropTarget<Item>(
DND_GROUP,
{
// drop 時のコールバック
drop(dropProps, monitor, _dropComponent) {
if (monitor) {
const dragProps: {
key: string
id: string
onDrop: (from: string, to: string) => void
} = monitor.getItem() as any // DragSource の props が取り出せる
if (dropProps.id !== dragProps.id) {
dragProps.onDrop(dragProps.id, dropProps.id)
}
}
}
},
connect => {
return {
connectDropTarget: connect.dropTarget()
}
}
),
DragSource<Item>(
DND_GROUP,
{
beginDrag(props) {
return props
}
},
(connect, monitor) => {
return {
connectDragSource: connect.dragSource(),
isDragging: monitor.isDragging()
}
}
)
)((props: any) => {
return props.connectDragSource(
props.connectDropTarget(<div>{props.children}</div>)
)
}) as any
function compose(...funcs: Function[]) {
return funcs.reduce((a, b) => (...args: any) => a(b(...args)));
}