メインコンテンツへ移動

React DnD

無料2018-03-02#JS#Solution#react-drag-drop#react拖放库#react拖拽#react-dnd tutorial#react拖拽教程

独自の視点で綿密に設計されたDnD(ドラッグ&ドロップ)機能ライブラリ

1. 設計理念

React DnD gives you a set of powerful primitives, but it does not contain any readymade components. It's lower level than jQuery UI or interact.js and is focused on getting the drag and drop interaction right, leaving its visual aspects such as axis constraints or snapping to you. For example, React DnD doesn't plan to provide a Sortable component. Instead it makes it easy for you to build your own, with any rendering customizations that you need.

Non-Goals より抜粋)

端的に言えば、DnD機能をいくつかの基礎的なインターフェース(掴めるもの=大根、放せる容器=穴)に分解し、それらのインターフェースを実装したインスタンスにDnDの内部状態を公開します。一般的な業務シーンに対応するためのDraggable Componentを際限なく提供する他のライブラリとは異なり、React DnDは比較的低レイヤーの視点からサポートを提供します。これはドラッグ&ドロップ機能の抽象化とカプセル化であり、抽象化によって使用法を簡素化し、カプセル化によって下層の違いを隠蔽します。

2. 用語と概念

Backend

HTML5 DnD API は互換性があまり良くなく、モバイル端末にも適していません。そのため、DnDに関連する具体的なDOMイベントを切り出し、単独の層として分離しました。これがBackendです:

Under the hood, all the backends do is translate the DOM events into the internal Redux actions that React DnD can process.

ItemとType

Itemは要素やコンポーネントを抽象化したもので、ドラッグ&ドロップの対象はDOM要素やReactコンポーネントそのものではなく、特定のデータモデル(Item)です:

An item is a plain JavaScript object describing what's being dragged.

このような抽象化を行うのも、疎結合を保つためです:

Describing the dragged data as a plain object helps you keep the components decoupled and unaware of each other.

TypeとItemの関係はClassとClass Instanceに似ており、Typeは同種のItemを表すための一意の識別子として機能します:

A type is a string (or a symbol) uniquely identifying a whole class of items in your application.

Typeはドラッグソース(大根)とドロップターゲット(穴)をマッチングさせる根拠となり、従来のDnDライブラリにおける group name に相当します。

Monitor

Monitorはドラッグ&ドロップの状態の集合体です。例えば、ドラッグ操作が進行中か、進行中であればどのアイテムがドラッグされ、どのターゲットの上にあるか、といった情報です:

React DnD exposes this state to your components via a few tiny wrappers over the internal state storage called the monitors.

例:

monitor.isDragging()
monitor.isOver()
monitor.canDrop()
monitor.getItem()

Reduxの mapStateToProps のように、 props インジェクションを通じてDnDの内部状態を公開します:

export default DragSource(Types.CARD, cardSource, (connector, monitor) => ({
  // You can ask the monitor about the current drag state:
  isDragging: monitor.isDragging()
}))(Card);

P.S. 実際、React DnDはReduxに基づいて実装されています。詳細は後述のコア実装セクションを参照してください。

Connector

Connectorは、DOMの抽象(React)とDnD Backendが必要とする具体的なDOM要素との間の繋がりを確立するために使用されます:

The connectors let you assign one of the predefined roles (a drag source, a drag preview, or a drop target) to the DOM nodes

使い方が非常に面白いです:

render() {
  const { highlighted, hovered, connectDropTarget } = this.props;

  // 1. DnDロールに対応するDOM要素を宣言する
  return connectDropTarget(
    <div className={classSet({
      'Cell': true,
      'Cell--highlighted': highlighted,
      'Cell--hovered': hovered
    })}>
      {this.props.children}
    </div>
  );
}

// 2. connectorからconnectメソッドを取り出し、propsに注入する
export default DragSource(Types.CARD, cardSource, (connector, monitor) => ({
  // Call this function inside render()
  // to let React DnD handle the drag events:
  connectDropTarget: connector.dropTarget()
}))(Card);

繋がりを作る部分 connectDropTarget(<div/>)非常にエレガントに見えます。実際の仕組みはおそらく以下と同等でしょう:

render() {
  const { connectToRole } = this.props;
  return <div ref={(node) => connectToRole(node)}></div>
}

正解でした:

Internally it works by attaching a callback ref to the React element you gave it.

Drag SourceとDrop Target

これら2つはDnDロール(DnDにおける役割)と呼ぶことができ、ドラッグソースとドロップターゲットの他に、 drag preview と呼ばれるものもあります。これは通常、別の状態のドラッグソースと見なすことができます。

DnDロールはReact DnDにおける基本的な抽象化ユニットです:

They really tie the types, the items, the side effects, and the collecting functions together with your components.

これは、Type、DnDイベントハンドラ(ドロップターゲットなら通常 hoverdrop イベントの処理が必要)など、その役割に関連する記述とアクションの集合体です。

3. コア実装

./packages
├── dnd-core
├── react-dnd
├── react-dnd-html5-backend
└── react-dnd-test-backend

論理構造は以下のようになっています:

API (React接続層)
  react-dnd: Contextの定義、ProviderやContainer factoryなどの上位APIを提供
-------
Core (抽象化層:インターフェースの定義)
  dnd-core: ActionやReducerを定義し、上下の層を接続
-------
Backends (ネイティブ接続層:インターフェースの実装)
  react-dnd-xxx-backend: 具体的な環境に接続し、Dispatch Actionを通じてネイティブのDnD状態を上位層に伝達

これはReduxに基づくロジックの分解と見なすことができます。中間層のCoreがDnDの状態を保持し、下層のBackendsが約束されたインターフェースを実装してCoreのデータソースとなり、上層のAPIがCoreから状態を取り出して業務層に渡します。

4. 基本的な使い方

1. DragDropContextの指定

アプリのルートコンポーネントにDragDropContextを宣言します。例:

import { DragDropContext } from 'react-dnd';
import HTML5Backend from 'react-dnd-html5-backend';

class App extends Component {}
export default DragDropContext(HTML5Backend)(App);

2. DragSourceの追加

DragSource高階コンポーネントは3つの引数( type , spec , collect() )を受け取ります。例:

export const ItemTypes = {
  KNIGHT: 'knight'
};

const knightSpec = {
  beginDrag(props) {
    // Itemの構造を定義し、monitor.getItem()で読み取る
    return {
      pieceId: props.id
    };
  }
};

function collect(connector, monitor) {
  return {
    connectDragSource: connector.dragSource(),
    isDragging: monitor.isDragging()
  }
}

最後にComponent/Containerと接続します(Reduxの connect() と同様):

export default DragSource(ItemTypes.KNIGHT, knightSpec, collect)(Knight);

コンポーネントは注入されたDnD状態を受け取ってUIをレンダリングします。例:

render() {
  const { connectDragSource, isDragging } = this.props;
  return connectDragSource(
    <div style={{
      opacity: isDragging ? 0.5 : 1,
      cursor: 'move'
    }} />
  );
}

ドラッグされた際の効果(ドラッグ対象が半透明になるなど)が自然に実現されています。複雑なDnD処理ロジック(これらはReact DnD Backendにカプセル化されています)を見る必要はなく、業務に必要なDnD状態だけが公開されます。

3. DropTargetの追加

同様に3つの引数( type , spec , collect() )が必要です:

const dropSpec = {
  canDrop(props) {
    return canMoveKnight(props.x, props.y);
  },

  drop(props, monitor) {
    const { id } = monitor.getItem();
    moveKnight(id, props.x, props.y);
  }
};

function collect(connector, monitor) {
  return {
    connectDropTarget: connector.dropTarget(),
    isOver: monitor.isOver(),
    canDrop: monitor.canDrop()
  };
}

最後に接続します:

export default DropTarget(ItemTypes.KNIGHT, dropSpec, collect)(BoardSquare);

コンポーネントは注入されたこれらのDnD状態を取得してUIを表示します。例:

render() {
  const { connectDropTarget, isOver, canDrop } = this.props;

  return connectDropTarget(
    <div>
      {isOver && !canDrop && this.renderOverlay('red')}
      {!isOver && canDrop && this.renderOverlay('yellow')}
      {isOver && canDrop && this.renderOverlay('green')}
    </div>
  );
}

ドラッグ操作の妥当性に応じて穴の色が変わる効果も、同じように自然に実現されています。

4. DragPreviewのカスタマイズ

ブラウザのDnDはデフォルトでドラッグ中の要素に基づいたdrag preview(通常は半透明のスクリーンショットのようなもの)を作成します。カスタマイズしたい場合は、DragSourceの作成方法と似ています:

function collect(connector, monitor) {
  return {
    connectDragSource: connector.dragSource(),
    connectDragPreview: connector.dragPreview()
  }
}

注入された connectDragPreview() を使ってDragPreviewをカスタマイズします。インターフェースのシグネチャは connectDragSource() と同じく dragPreview() => (elementOrNode, options?) です。例えば、よくあるドラッグハンドル(handle)の効果は以下のように実装できます:

render() {
  const { connectDragSource, connectDragPreview } = this.props;

    return connectDragPreview(
      <div
        style={{
          position: 'relative',
          width: 100,
          height: 100,
          backgroundColor: '#eee'
        }}
      >
        Card Content
        {connectDragSource(
          <div
            style={{
              position: 'absolute',
              top: 0,
              left: '100%'
            }}
          >
            &lt;HANDLE&gt;
          </div>
        )}
      </div>
  );
}

また、 Image オブジェクトをDragPreviewとして使うこともできます(IEは非対応):

componentDidMount() {
  const img = new Image();
  img.src = 'http://mysite.com/image.jpg';
  img.onload = () => this.props.connectDragPreview(img);
}

5. オンラインデモ

Githubリポジトリ: ayqy/example-react-dnd-nested

オンラインデモ: https://ayqy.github.io/dnd/demo/react-dnd/index.html

参考資料

  • React DnD:Reduxのドキュメントと同様に、非常に美しく、読み進める手が止まらない素晴らしいドキュメントです。

  • react-dnd/react-dnd

コメント

コメントはまだありません

コメントを書く