react-dnd これだけ抑えておけばおｋ

巷で難しい難しいと言われていた react-dnd でドラッグアンドドロップを実装してみたところ、案外楽だったのでまとめる。

## 用語整理

- `DragSource` : マウスで掴んでドラッグする対象
- `DropTarget` : ドロップ対象
- `monitor` :  drop/hover時に今現在のイベント対象の状態を取り出せるもの


## 使い方

- `@DragDropContext` を ドラッグアンドドロップしたい領域のコンポーネントに注入する
- `@DropTarget` を落としたい先のコンポーネントに注入する
- `@DragSource` をドラッグさせたいコンポーネントに注入する

たとえばドラッグアンドドロップでリストの要素を入れ替えたい場合、 リスト全体が `DragDropContext` で、 リスト要素が `DragSource` かつ `DropTarget` になる。


## 実装例

並び替えられるリスト要素の実装

```js
@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>
    ));
  }
}
```


リストの実装

```js

@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版も書いておく。デコレータは使わない。正直、型はうまくつかない。

```tsx
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)));
}
```