#まえがき
React DnDの和訳ドキュメントがなさげので、自分用に私訳した記録です。
私訳なので英文の直訳を自分なりに解釈し記述した部分が結構あります。
むしろ内容すら間違ってる。と、見兼ねてつっこみたくなる箇所がありましたら、どうぞ宜しくお願いします。
#Overview : 概要
本家ページ: Overview - React DnD
##Items and Types
fluxと同じく、React DnDはデータをviewとしてではなく、事実上のソースとして使用します。
画面上で何かしらのデータをDragした際、そのデータをコンポーネントやDOMノードとしては扱わず、代わりにDragされているデータを特定のタイプのアイテム("item")として扱います。
###■ "item"とは?
現在Dragされているものがどのようなものなのかを、プレーンなJavascriptオブジェクトで説明しているものです。
例えば...
カンバンボードアプリにおいて、あるカードをDragした際、そのカードは{ cardId: 42 }
というオブジェクトとして扱われます。
チェスゲームにおいては、女王(駒)をC5から動かした際、その駒は{fromCell: 'C5', piece: 'queen' }
というオブジェクトとして扱われます。
DragされたデータをプレーンなJSオブジェクトとして扱うことによって、各コンポーネント同士が互いに干渉せずに分離されている状態を維持することができます。
###■ "type"とは?
アプリケーション中のitemのすべてのclassを一意に見分ける文字列、またはシンボルです。
カンバンボードアプリ中では...
'card'type = Dragできるカードを再定義
'list'type = それらのカードのDrag可能なものリストとして存在
"type"はアプリケーションを成長させる過程で、より多くのものをDragさせたい場合に有用なライブラリです。
しかし、全ての既存のDragターゲットを新しいitemへとreact(反応)させようとする必要は必ずしもありません。(?)
"type"によって、どのDragソースとDropターゲットが適合するのかを指定することができるのです。
おそらく、FluxのAction typeのエミュレーションと同様に、アプリケーション中にtypeのエミュレーション定数を持つことになるでしょう。
##Monitor
Drag&Dropは、本質的にステートフルです(状態を持っている)。
Dragオペレーションが進行中か否か。
現在の"type"や"item”があるか否か。
何かしらの状態を、常に有していなければなりません。
React DnDではこの状態を幾つかの簡単なラッパーを通じて"monitor"と呼ばれるインターナルステートストレージ(内部状態ストレージ)に反映させます。
"monitor"は、Drag&Dropの状態変化に応じてコンポーネントのpropsをアップデートします。
###■ collect関数
Drag&Dropの状態を追跡する必要のある各コンポーネントには、"monitors"から関連したbitを取得するためのcollect関数を定義することができます。
状態変化の際は、タイムリーにcollect関数を呼び出し、コンポーネントのpropsにその返り値をマージ(結合)させます。
####例:ハイライト
チェスの駒がDragされた際に、チェスボードのセルをハイライトしたい場合を考えます。
Cell
コンポーネント用のcollect関数は以下のようになります。
function collect(monitor) {
return {
highlighted: monitor.canDrop(),
hovered: monitor.isOver()
};
}
ここでは、propsとしてhighlighted
とhoverd
の最新の値を全てのCell
インスタンスに渡すようにReact DnDに指示しています。
##Connector
バックエンドがDOMイベントを操作する際にコンポーネントがReactを使用してDOMを記述していたら、バックエンドはどちらのDOMノードを採用するか、どのように判断するのでしょうか?
ここで、"Connector"を使用します。
Connectorはrender()
function内のDOMノードにあらかじめ決められた役割(Dragソース、Dragプレビュー、Dragターゲット)を割り当てます。
Connectorは上記で定義したcollect関数の第1引数に渡されます。
Dragターゲットを記述するためにどのように使用するのかを見てみましょう。
function collect(connect, monitor) {
return {
highlighted: monitor.canDrop(),
hovered: monitor.isOver(),
connectDropTarget: connect.dropTarget()
};
}
コンポーネントのrender
メソッド中では、"Monitor"から取得した両方のデータや"Connector"から取得した関数にアクセスすることができます。
render: function () {
var highlighted = this.props.highlighted;
var hovered = this.props.hovered;
var connectDropTarget = this.props.connectDropTarget;
return connectDropTarget(
<div className={classSet({
'Cell': true,
'Cell--highlighted': highlighted,
'Cell--hovered': hovered
})}>
{this.props.children}
</div>
);
}
render() {
const { highlighted, hovered, connectDropTarget } = this.props;
return connectDropTarget(
<div className={classSet({
'Cell': true,
'Cell--highlighted': highlighted,
'Cell--hovered': hovered
})}>
{this.props.children}
</div>
);
}
connectDropTarget
はコンポーネントのルートDOMノードが有効なDropターゲットだということ、そのノードのhover&dropイベントがバックエンドによって処理されるべきだということをReact DnDに知らせます。
内部的には、あなたが与えたReact要素にcallback refをアタッチ(タスク生成)することで動いています。
"Connector"から返される関数はメモ化(※1)されます。よって、shouldComponentUpdate
の最適化を邪魔することはありません。
※1: メモ化=サブルーチンの呼び出し結果を後で再利用するために保持する。サブルーチンの呼び出しごとの再計算を防ぐ手法。
##Drag Sources and Drop Targets
これまでのところ、"Monitor"や"Connector"によって表現されているitemやtype、collect関数である、バックエンドで処理されるDOMやデータについてカバーし、どんなpropsをコンポーネントにセットすべきかを述べてきました。(??)
では、これらのpropsをセットするためにコンポーネントをどのように設定すれば良いでしょうか?
Drag&Dropイベントのレスポンスによる副作用をどう扱えば良いのでしょうか?
DragSourceとDropTarget、React DnDの主要な抽象ユニットを交えることで、コンポーネントにtype、items、副作用、collect関数を結束(?)させます。
コンポーネントやその一部をDrag可能にしたい場合は、DragSource宣言にコンポーネントをラップする必要があります。
全てのDrag sourceは特定の"type"に登録され、コンポーネントのpropsからitemを生成するメソッドを実装しなければなりません。
オプションとして、Drag&Dropイベントを操作する幾つかのメソッドも指定することができます。
DragSource宣言においては、渡されたコンポーネントに対するcollect関数を指定できます。
DropTargetは、DragSourceと非常に似ています。
異なる点は、single drop targetは一度に複数のitem typeに登録することができ、アイテムを生成する代わりに、hoverまたはdropを処理することができるという点です。
##Higher-Order Components and ES7 decorators
では、どのようにコンポーネントをラップするのでしょうか?
ラップすることは何を意味するのでしょうか?
もしこれまでにhigher-orderコンポーネントを使用したことがない場合は詳しいコンセプトが説明されているこちらの記事(英文)を御覧ください。
higher-orderコンポーネントとは単に、コンポーネントクラスを読みとり、別のコンポーネントクラスを返す関数です。
ラップしているコンポーネントは、有用な挙動を追加するだけでなく、コンポーネントをrender
メソッドにレンダリングし、そこにpropsを転送するライブラリによって提供されます。
React DnDにおいて、DragSourceとDragTargetは、他の幾つかのTop-Levelエクスポート関数と同様に、事実上のhigher-orderコンポーネントです。これらはあなたのコンポーネントにDrag&Dropマジックを与えてくれるでしょう。
これらを使用する上での注意点は、これらは2つの関数のアプリケーションを必要とすることです。
例として、ここでYourComponent
をどのようにDragSourceにラップするかを見てみましょう。
var DragSource = require('react-dnd').DragSource;
var YourComponent = React.createClass({
/* 処理 */
});
module.exports = DragSource(/* 処理 */)(YourComponent);
最初の関数呼び出しの、DragSourceパラメータを指定した後、2つ目の関数呼び出しで最終的にあなたのclassYourComponent
を渡しているという点に注意してください。
これは、currying(※1)にまたはPartial Applicaion(※2)によって呼び出され、またES7デコレータ構文をBox外で動作させるために必要な操作となります。
import { DragSource } from 'react-dnd';
@DragSource(/* 処理 */)
export default class YourComponent {
/* 処理 */
}
この構文を使う必要はありませんが、もし使用する際はコードをBabelにトランスパイル(※3)し、 .babelrc fileに{ "stage": 1 }
をセットすることにより、これを有効にできます。
もしES7を使用する予定がないのならば、partial application(部分適用)の利用をお勧めします。
ES5またはES6では幾つかのDragSourceとDropTargetの宣言を_.flow
という関数コンポジションヘルパーを使用して一つにまとめることができるためです。
ES7では、同様の効果を得るためにデコレータ(関数を修飾するための関数、仕組み)をスタックすることができます。
※1: カリー化(currying)=複数の引数をとる関数を、引数が「もとの関数の最初の引数」で戻り値が「もとの関数の残りの引数を取り結果を返す関数」であるような関数にすること。 :by Wikipedia
[参考]
食べられないほうのカリー化入門 - Qiita
JavaScriptのカリー化について味見してみる - Qiita
※2: 部分適用=一部の引数を固定化して新しい関数を作り出すこと
[参考]
カリー化と部分適用(JavaScriptとHaskell) - Qiita
※3: トランスパイラ=ES6をサポートしていないブラウザのためにES6で書かれたコードをES5のコードへ変換を行う仕組み。
[参考]
第1回 Babelとは - CodeGrid
var DragSource = require('react-dnd').DragSource;
var DropTarget = require('react-dnd').DropTarget;
var flow = require('lodash/flow');
var YourComponent = React.createClass({
/* 処理 */
});
module.exports = flow(
DragSource(/* 処理 */),
DropTarget(/* 処理 */)
)(YourComponent);
import { DragSource } from 'react-dnd';
import flow from 'lodash/flow';
class YourComponent {
/* 処理 */
}
export default flow(
DragSource(/* 処理 */)
DropTarget(/* 処理 */)
)(YourComponent);
import { DragSource } from 'react-dnd';
@DragSource(/* 処理 */)
@DropTarget(/* 処理 */)
export default class YourComponent {
/* 処理 */
}
##Putting It All Together
以下の例では、既存のCard
コンポーネントをDragSourceにラッピングしています。
var React = require('react');
var DragSource = require('react-dnd').DragSource;
// DragSourcesとDropTargetは
// 同じ文字列typeのときのみ相互に作用する。
// 残りのアプリの定数と一緒にtypeを別ファイルに保存したい場合
var Types = {
CARD: 'card'
};
/**
* drag source contractを指定する.
* beginDrag関数は必須
*/
var cardSource = {
beginDrag: function (props) {
// ドラッグされたアイテムが記述されたデータを返す
var item = { id: props.id };
return item;
},
endDrag: function (props, monitor, component) {
if (!monitor.didDrop()) {
return;
}
// 互換性のあるtargetにドロップした場合、何らかの動作をする。
var item = monitor.getItem();
var dropResult = monitor.getDropResult();
CardActions.moveCardToList(item.id, dropResult.listId);
}
};
/**
* コンポーネントにセットするpropsを指定する
*/
function collect(connect, monitor) {
return {
// React DnDにDragイベントを処理させるために、
// render()内でこの関数を呼び出します
connectDragSource: connect.dragSource(),
// 現在のDragステートをモニターに問い合わせる
isDragging: monitor.isDragging()
};
}
var Card = React.createClass({
render: function () {
// コンポーネントは通常どおり自身のpropsを受け取ることができる
var id = this.props.id;
// 上記のcollect関数で定義されているように、
// これら2つのpropsはReact DnDによってセットされる。
var isDragging = this.props.isDragging;
var connectDragSource = this.props.connectDragSource;
return connectDragSource(
<div>
I am a draggable card number {id}
{isDragging && ' (and I am being dragged now)'}
</div>
);
}
});
// ラップされたものをエクスポート
module.exports = DragSource(Types.CARD, cardSource, collect)(Card);
import React from 'react';
import { DragSource } from 'react-dnd';
const Types = {
CARD: 'card'
};
const cardSource = {
beginDrag(props) {
const item = { id: props.id };
return item;
},
endDrag(props, monitor, component) {
if (!monitor.didDrop()) {
return;
}
const item = monitor.getItem();
const dropResult = monitor.getDropResult();
CardActions.moveCardToList(item.id, dropResult.listId);
}
};
function collect(connect, monitor) {
return {
connectDragSource: connect.dragSource(),
isDragging: monitor.isDragging()
};
}
class Card {
render() {
const { id } = this.props;
const { isDragging, connectDragSource } = this.props;
return connectDragSource(
<div>
I am a draggable card number {id}
{isDragging && ' (and I am being dragged now)'}
</div>
);
}
}
export default DragSource(Types.CARD, cardSource, collect)(Card);
import React from 'react';
import { DragSource } from 'react-dnd';
const Types = {
CARD: 'card'
};
const cardSource = {
beginDrag(props) {
const item = { id: props.id };
return item;
},
endDrag(props, monitor, component) {
if (!monitor.didDrop()) {
return;
}
const item = monitor.getItem();
const dropResult = monitor.getDropResult();
CardActions.moveCardToList(item.id, dropResult.listId);
}
};
// Use the decorator syntax
@DragSource(Types.CARD, cardSource, (connect, monitor) => ({
connectDragSource: connect.dragSource(),
isDragging: monitor.isDragging()
}))
export default class Card {
render() {
const { id } = this.props;
const { isDragging, connectDragSource } = this.props;
return connectDragSource(
<div>
I am a draggable card number {id}
{isDragging && ' (and I am being dragged now)'}
</div>
);
}
}