React
react-dnd

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

巷で難しい難しいと言われていた 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