ReactでDrag&Drop

ReactDnDについて

Reactでdrag&dropコンポーネントを実装するのにおそらく一番有名(Redux作った人が作った)かつドキュメントが豊富なパッケージです。ドキュメントの情報量が結構多く自由度が高くて混乱しやすいので軽く使ってみたい人向けに核となるところだけ解説します。Danさん本当好き。

API

各コンポーネントをdrggable&droppable化するためのAPIがES7のdecoratorとして提供されています。
babel6使っている人はdecoratorがまだ公式では対応していないみたいなので注意してください(babel5なら大丈夫です)。公式じゃなければbabel6用のプラグイン作っている人がいたと思うので探してみてください。

DragSource

ドラッグされるコンポーネントについての設定を行える。

import React from "react";
import { DragSource } from "react-dnd";
@DragSouce(type, spec, collect)
export default class DragComponent extends React.Component {
// something
}

type: dorpされる側はここで設定したtypeを見てdropを受け入れるか否かを決める、SymbolかString。

spec: drag開始時の処理、drag終了時(dropされた時)の処理とかを書いたObject。例が後半にあります。

collect: DragComponent内で使う関数を取り出す関数でobjectを返す必要があります。connectとmonitorが引数として渡されます。ざっくり言うとconnectはDOMについて、monitorはdrag&dropの状態についてのObjectです、結構色々取れるので公式ドキュメント見てみてください。

DropTarget

ドロップされるコンポーネントについての設定を行える。

import React from "react";
import { DropTarget } from "react-dnd";
@DropTarget(types, spec, collect)
export default class DropComponent extends React.Component {
// something
}

types: dropを受け入れるtypeを設定する、SymbolかStringかArray。

spec: dropを受け入れた時の処理やhoverされている時の処理を書いたObject。例が後半にあります。

collect: DragSourceのcollectと同じ

DragDropContext

上記のコンポーネント達をこいつでラップすることで初めてdrag&dropができるようになる。Backendが必要。

import React from "react";
import HTML5Backend from "react-dnd-html5-backend";
import { DragDropContext } from "react-dnd";
@DragDropContext(HTML5Backend)
export default class DnDComponent extends React.Component {
// something
render() {
return (
// DragComponent & DropComponent
)
}
}

例ではHTML5Backendを使っているのでタッチには対応していません。タッチ対応のBackendもあるのでタッチ対応させたい人は探してみてください。

実装例

drag&dropするとメッセージを表示するコンポーネントを作ってみます。

DnDItem Componentを作る

実際にdrag&dropされるコンポーネントを作ります。
DragSourceとDropTargetは同時に使うことでdragもdropもできるコンポーネントを作ることが可能です。

drag&dropされたときのactionは親からpropsとして受け取ります。
またtypeも同様に親からpropsとして受け取っています。

DragSourceとDropTarget間は基本的にmonitorを通して値のやり取りをします。

import React from "react";
import { DragSource, DropTarget } from "react-dnd";
const dragSpec = {
// dragが始まったときの処理
beginDrag(props) {
// dragされ始めたら自分のidを返す
const { id } = props;
return { id };
},
// dragが終わったときの処置
endDrag(props, monitor) {
// beginDragで返されたidを取ってくる
const source = monitor.getItem();
// dropSpecのdropで返されたidを取ってくる
const target = monitor.getDropResult();
// dropActionを発火させる
if (target) props.dropAction(source.id, target.id);
}
}
const dropSpec = {
// dropされたときの処理
drop(props, monitor, component) {
// dropされたら自分のidを返す
const { id } = props;
return { id };
}
}
// DropTargetとDragSourceを使っているのでdragもdropもできる
@DropTarget(props => props.type, dropSpec, connect => ({ connectDropTarget: connect.dropTarget() }))
@DragSource(props => props.type, dragSpec, connect => ({ connectDragSource: connect.dragSource() }))
export default class DnDItem extends React.Component {
static propTypes = {
connectDragSource: React.PropTypes.func.isRequired,
connectDropTarget: React.PropTypes.func.isRequired,
dropAction: React.PropTypes.func.isRequired,
id: React.PropTypes.string.isRequired,
name: React.PropTypes.string.isRequired
};
render() {
const {
connectDragSource,
connectDropTarget,
name
} = this.props;
return connectDragSource(connectDropTarget(
<li>
<h3>{name}</h3>
</li>
));
}
}

DnDField Componentを作る

DnDItem Componentをラップする親玉を作ります、stateはこのコンポーネントで管理して更新用のactionをDnDItemに渡します。

今回はidをkeyとして渡しています、keyはReactDnD内部でのDOMの参照に使われるのでValueObjectである必要があります。あまりにも適当なものを渡すと壊れるので気をつけてください(React始めたばっかりの時Math.random()とか渡してて死にそうになった)。

import React from "react";
import HTML5Backend from "react-dnd-html5-backend";
import { DragDropContext } from "react-dnd";
import DnDItem from "./DnDItem";
// DragDropContextでラップする
@DragDropContext(HTML5Backend)
export default class DnDField extends React.Component {
constructor(props) {
super(props);
this.state = {
list: [
{id: "1", name: "foo"},
{id: "2", name: "bar"},
{id: "3", name: "bad"},
{id: "4", name: "qux"}
],
message: ""
};
}
dropAction(sourceId, targetId) {
const { list } = this.state;
// message更新 Redux使うならここでaction, それぞれのidが渡ってくる
const sourceName = list.find(item => item.id === sourceId).name;
const targetName = list.find(item => item.id === targetId).name;
this.setState({message: `${sourceName} dropped on ${targetName}`});
}
render()
const { list, message } = this.state;
const itemType = Symbol("item");
return (
<div>
<h1>{message}</h1>
<ol>
{list.map(item =>
<DnDItem id={item.id} name={item.name} type={itemType} dropAction={::this.dropAction} key={item.id}/>
)}
</ol>
</div>
)
}
}

まとめ

これでdragされたComponentのidとdropされたComponentのidが取得できる実装が出来ました。idが取得できればあとはどうにでもできるので実際の開発に生かすことができればと思います。

今回紹介したのはReactDnDのほんの一部で他にもたくさんのオプションがあるので是非ドキュメントを読んでカスタムしてみてください。