WebDesign Dackel

Reactでドラッグアンドドロップを実装したいので、react-dndを使ってみた

Reactでドラッグアンドドロップを実装したいので、react-dndを使ってみた

Hatena0
Google+0
Pocket0
Feedly0

やりたいこと

例えば、TODO管理のWebアプリでタスクの並び替えを実装しよう!となった時、ドラッグ&ドロップを使って順番を入れ替えたいですよね。

ただ、React.jsでは基本的に直接DOM操作は行わないので、jQuery UIsortableみたいなのは極力使いたくありません。(そもそもできるの?)
そこで、探してみたらreact-dndというライブラリが良さそうだったので、早速こちらを使って、リストのドラッグ&ドロップで入れ替えを実装してみました。

今回のファイルはGitHubへあげてますので、興味のある方は下記よりご確認下さい。

tsuyoshiwada/sample-react-dnd

動作サンプル

動作サンプル

実際に動作しているサンプルはこちら

各リストアイテムの左側にあるグレーの矩形をつかむことでドラッグ&ドロップが出来るようになっています。

後で書きますが、今回使用するreact-dndのバージョン1.1.1時点では標準でスマホなどのタッチデバイスへ対応していません。
スマホからアクセス頂いた方は申し訳ありませんが、PCからご確認下さいませ。。。

実装してみる

使用したライブラリと、バージョンは下記のとおりです。

  • react0.13.3
  • react-dnd1.1.1
  • classnames2.1.2

classnamesに関しては必須ではありませんが、Addonとして提供されるclassSetが非推奨になっているみたいなので、代わりのライブラリとして使用しています。

JedWatson/classnames

コードの全体

下記webpack+cjsx-loaderを使ってCoffeeScriptでかいていきます。

React           = require("react")
HTML5Backend    = require("react-dnd/modules/backends/HTML5")
DnD             = require("react-dnd")
DragDropContext = DnD.DragDropContext
DragSource      = DnD.DragSource
DropTarget      = DnD.DropTarget
classNames      = require("classNames")


# ドラッグ元のインターフェースを持たせる
itemSource = 
  beginDrag: (props) ->
    id: props.id

# ドラッグ元の機能と独自コンポーネントをつなぐ
collectSource = (connect, monitor) ->
  connectDragSource : connect.dragSource()
  connectDragPreview: connect.dragPreview()
  isDragging        : monitor.isDragging()

# ドラッグ先も同様にインターフェースとコンポーネントの結びつきを用意
itemTarget = 
  hover: (props, monitor) ->
    draggedId = monitor.getItem().id
    if draggedId != props.id
      props.moveItem(draggedId, props.id)
    return

collectTarget = (connect) ->
  connectDropTarget: connect.dropTarget()


# アイテムコンポーネント
Item = React.createClass(
  propTypes:
    connectDragSource : React.PropTypes.func.isRequired
    connectDragPreview: React.PropTypes.func.isRequired
    connectDropTarget : React.PropTypes.func.isRequired
    isDragging        : React.PropTypes.bool.isRequired
    id                : React.PropTypes.any.isRequired
    text              : React.PropTypes.string.isRequired
    moveItem          : React.PropTypes.func.isRequired

  render: ->
    classes = classNames(
      "list-group-item": true
      "dragging"       : @props.isDragging
    )

    @props.connectDragPreview(@props.connectDropTarget(
      <div className={classes}>
        {@props.connectDragSource(
          <span className="list-group-item__handle"></span>
        )}
        {@props.text}
      </div>
    ))
)

# ドラッグ&ドロップの機能を`Item`コンポーネントにかぶせる
Item = DropTarget("item", itemTarget, collectTarget)(Item)
Item = DragSource("item", itemSource, collectSource)(Item)


# Appコンポーネント (ルート)
App = React.createClass(
  getInitialState: ->
    items: [
      {id: 1, text: "I am item01"}
      {id: 2, text: "I am item02"}
      {id: 3, text: "I am item03"}
      {id: 4, text: "I am item04"}
      {id: 5, text: "I am item05"}
      {id: 6, text: "I am item06"}
    ]

  # 要素の入れ替え
  handleMoveItem: (id, afterId) ->
    items = @state.items.concat()

    item = (obj for obj in items when obj.id == id)[0]
    itemIndex = items.indexOf(item)

    afterItem = (obj for obj in items when obj.id == afterId)[0]
    afterItemIndex = items.indexOf(afterItem)

    items[itemIndex] = afterItem
    items[afterItemIndex] = item

    @setState(items: items)

  render: ->
    items = @state.items.map((item) =>
      <Item key={item.id}
            id={item.id}
            text={item.text}
            moveItem={@handleMoveItem} />
    )

    <div className="container">
      <h1>React.jsのドラッグ&ドロップサンプル</h1>
      <div className="panel panel-default">
        <div className="panel-heading">
          <h2 className="panel-title">ドラッグ&ドロップのリスト</h2>
        </div>
        <div className="panel-body">
          下記のリストアイテムはドラッグ可能です!
        </div>
        <ul className="list-group">
          {items}
        </ul>
      </div>
    </div>
)

# Appコンポーネントをドラッグ&ドロップのコンテキストとする
App = DragDropContext(HTML5Backend)(App)


React.render(<App />, document.getElementById("app"))

前回と同様にとりあえず一つのファイルにずらーっと書いてしまいました。

実装のポイントは、ドラッグしたい要素とドラッグ先(上記ではItemコンポーネント)に対して、react-dndの提供する機能をかぶせるあたりかなと思います。

ドラッグ時のスタイルを設定

上記のItem#render()内で要素に対して、ドラッグしている最中はdraggingクラスというのを付与しています。
そのクラスに対してスタイルを当てて、ドラッグ時のスタイルを調整できます。

.dragging {
  opacity:.3;
}

地味な注意点として、ドラッグしている要素そのものでは無く、ドラッグをし始めた要素に対してスタイルが当たる点に注意です。
(凄くわかりづらい説明ですみません…)

ハンドルでは無く要素全体を掴めるようにする

先ほどのサンプルでは、左側にあるグレーの矩形(ハンドル)だけに操作を対応させていました。
しかし、要素全体に対応したい場合もありますので、そんな時はItem#render()をちょっと変えます。

Item = React.createClass(
  # ...
  render: ->
    classes = classNames(
      "list-group-item": true
      "dragging"       : @props.isDragging
    )

    # 全体のドラッグ操作を許す場合
    @props.connectDragSource(@props.connectDropTarget(
      <div className={classes}>
        <span className="list-group-item__handle"></span>{@props.text}
      </div>
    ))
)

変更箇所は、ハンドル付きの場合に使用していたconnectDragPreview()を無くしている点だけです。

ハンドル無しの動作サンプル

react-dndを選んだ理由

React Componentsでドラッグ&ドロップが出来そうなものを探した時にもっともstarが多かった!というのも勿論ですが、react-dndは拡張性がとても高く、色々な場面で使えそうだと思ったからです。
また、プロジェクトページに沢山のサンプルがある点も魅力的でした。(ここ相当重要ですよね!)

注意点

冒頭でも書きましたがreact-dnd1.1.1ではタッチデバイスへがサポートされていません。

標準では対応していませんが、こちらにある200行程度のコードで対応させられるみたいです。
しかし、今回どうしても動かせなかったので見送りました。笑
今後のバージョンアップで標準的なサポートを予定しているみたいなので今後に期待です。

まとめ

今回記事に書いたのはシンプルな内容でしたが、こってこてに使い倒したい!という方はreact-dndのサンプルが参考になるかと思います。