HTML5テクニカルノート
Create React App + React DnD 02: ドラッグ&ドロップで動かす
- ID: FN2004001
- Technique: ECMAScript 2015
- Library: React 17.0.2 / React DnD 14.0.2
React DnDサイトの「Tutorial」でつくられるドラッグ&ドロップのサンプルコードに、少し手を加えて解説し直すチュートリアルの第2回です。いよいよ、ドラッグしてみます。
01 コンポーネントをドラッグする
まず、React DnDをインストールしなければなりません。React DnDは、HTMLドラッグ&ドロップAPIにもとづいてマウス操作を扱います。そのために必要になるのが、バックエンドと呼ばれるプラグインです。そこで、react-dnd-html5-backend
もインストールに加えてください。
npm install react-dnd react-dnd-html5-backend
なお、このバックエンドはタッチスクリーンでは使えません。タッチ操作には、バックエンドとしてreact-dnd-touch-backend
を用います。
アプリケーションにReact DnDの機能を与えるのが、DndProvider
コンポーネントです。バックエンド(HTML5Backend
)は、backend
プロパティに定めてください。クリックイベントハンドラは、もう外してしまいます。
src/components/Board.jsimport { DndProvider } from 'react-dnd'; import { HTML5Backend } from 'react-dnd-html5-backend' // import { canMoveKnight, moveKnight } from '../Game'; function renderSquare(i, [knightX, knightY]) { return ( <div key={i} style={squareStyle} // onClick={(() => handleSquareClick(x, y))} > </div> ); } /* function handleSquareClick(toX, toY) { if (canMoveKnight(toX, toY)) { moveKnight(toX, toY); } } */ function Board({ knightPosition }) { return ( <DndProvider backend={HTML5Backend}> <div style={boardStyle}> {squares} </div> </DndProvider> ); }
React DnDがドラッグする対象は、コンポーネントでもDOMノードでもなく、item
と呼ばれるオブジェクトつまりデータです。item
は識別のためのtype
というプロパティを必ず備えます。ほかに、ドラッグ対象についての情報をプロパティに含めても構いません(ただし、あまり詰め込みすぎないように)。type
の値は文字列(またはsymbol
)で、型決めのモジュールに定数として定めることがお勧めです(コード001)。
コード001■型を定数として定めるモジュール
src/ItemTypes.js
export const ItemTypes = {
KNIGHT: 'knight',
}
ここでReact DnDのuseDrag
フック(hook)を使います。引数はコールバック関数で、返されるのがドラッグ操作に必要な仕様を収めたオブジェクトです。プロパティにはtype
とcollect()
関数を加えました。type
は、上述のitem
オブジェクトを識別するプロパティです。関数collect()
は、引数のmonitor
からドラッグするオブジェクトの状態として用いるプロパティを取り出し、オブジェクトにして返します。
useDrag
フッククは配列を返します。はじめの要素はcollect()
関数の集めたプロパティが収められたオブジェクトです。monitor
オブジェクトのisDragging()
メソッドは、ドラッグ中かどうかを論理値で返します(ノート01)。ふたつ目の要素(drag
)がドラッグしているオブジェクトとDOMを結ぶconnector()
関数です。この関数は、ドラッグするJSX要素のref属性に与えてください。
src/components/Knight.jsimport { useDrag } from 'react-dnd'; import { ItemTypes } from '../ItemTypes'; const Knight = () => { const [{ isDragging }, drag] = useDrag(() => ({ type: ItemTypes.KNIGHT, collect: (monitor) => ({ isDragging: !!monitor.isDragging(), }), })); return ( // <div style={knightStyle}> <div ref={drag} style={{ ...knightStyle, opacity: isDragging ? 0.5 : 1, }} > ♘ </div> ) };
ノート01■二重の論理否定演算子
論理否定演算子!
をふたつ添えると、true
はtrue
、false
はfalse
のまま変わりません。けれど、オペランド(被演算子)がブール(論理)値でないとき、ひとつめの演算子でブール値評価されたうえで否定(反転)されます。つまり、結果を必ずブール値にすることが目的です。
これでドラッグはできるものの、ドロップできません(図001)。マウスボタンを放せば、もとの位置に戻ってしまいます。つぎにドロップ先を決めなければならないのです。useDrag
フックについては、つぎに簡単にまとめておきます。引数などは、今回扱う範囲でご紹介しました。詳しくは、公式サイトのuseDrag
をお読みください。
構文001■useDragフック
useDrag |
|
引数 |
|
戻り値 |
ドラッグの内容を表す配列。
|
図001■ドラッグできてもドロップするともとの位置に戻ってしまう
02 コンポーネントをドロップする
ナイトを動かすときのマス目の位置情報は、これまで盤面のコンポーネント(Board
)が担ってきました。ドラッグ&ドロップを扱おうとすると、コードが込み入ってきます。そこで、盤面から位置情報にもとづく処理を切り分けましょう。
もっとも、位置情報の処理をマス目(Square
)に移したのでは、切り分けたことになりません。マス目を包む新たな位置情報のコンポーネント(BoardSquare
)を、このあとつくることにします。
src/components/Board.js// import Square from './Square'; import BoardSquare from './BoardSquare'; function renderSquare(i, [knightX, knightY]) { // const black = (x + y) % 2 === 1; // const isKnightHere = knightX === x && knightY === y; // const piece = isKnightHere ? <Knight /> : null; return ( <div key={i} style={squareStyle}> {/* <Square black={black}>{piece}</Square> */} <BoardSquare x={x} y={y}> {renderPiece(x, y, [knightX, knightY])} </BoardSquare> </div> ); } function renderPiece(x, y, [knightX, knightY]) { const isKnightHere = knightX === x && knightY === y; return isKnightHere ? <Knight /> : null; }
ドロップの動きを決めるのは、React DnDのuseDrop
フックです。引数はコールバック関数で、ドロップ操作に必要な仕様が収められたオブジェクトを返します。プロパティaccept
の値は、ドロップできるitem
オブジェクトのtype
です。そして、受け入れ可能な項目がドロップされると、仕様オブジェクトに定めたdrop()
メソッドが呼び出されます。フックの戻り値は配列で、ふたつ目の要素はドロップ先オブジェクトとDOMを結ぶconnector()
関数です。ドロップするJSX要素のref
属性に与えてください。
新たなモジュールsrc/components/BoardSquare.js
の定めは、つぎのとおりです。これで、ナイトがドラッグ&ドロップできるようになります。ただし、ドロップ先が適切かどうかは判定していません。そのため、どのマス目にもドロップできます(図002)。
src/components/BoardSquare.jsimport React from 'react'; import { useDrop } from 'react-dnd'; import { moveKnight } from '../Game'; import { ItemTypes } from '../ItemTypes'; import Square from './Square'; const boardSquareStyle = { position: 'relative', width: '100%', height: '100%', }; const BoardSquare = ({ x, y, children }) => { const black = (x + y) % 2 === 1; const [, drop] = useDrop(() => ({ accept: ItemTypes.KNIGHT, drop: () => moveKnight(x, y), }), [x, y]); return ( <div ref={drop} style={boardSquareStyle} > <Square black={black}>{children}</Square> </div> ); }; export default BoardSquare;
この機会に、ナイトの駒のスタイルも少し整えましょう。
src/components/Knight.jsconst knightStyle = { fontSize: 60, /* 40, */ textAlign: 'center', lineHeight: '4rem', };
図002■盤面のどのマス目にもドロップできる
useDrop
フックについて、つぎに簡単にまとめておきます。引数などは、今回扱う範囲でご紹介しました。詳しくは、公式サイトのuseDrop
をお読みください。盤面のモジュールのスクリプト全体は、以下のコード002のとおりです。
構文002■useDropフック
useDrop |
|
引数 |
|
戻り値 |
ドラッグの内容を表す配列。
|
コード002■盤面のモジュール
src/components/Board.js
import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend'
import BoardSquare from './BoardSquare';
import Knight from './Knight';
const boardStyle = {
width: 500,
height: 500,
border: '1px solid gray',
display: 'flex',
flexWrap: 'wrap'
};
const squareStyle = { width: '12.5%', height: '12.5%'};
function renderSquare(i, [knightX, knightY]) {
const x = i % 8;
const y = Math.floor(i / 8);
return (
<div
key={i}
style={squareStyle}
>
<BoardSquare x={x} y={y}>
{renderPiece(x, y, [knightX, knightY])}
</BoardSquare>
</div>
);
}
function renderPiece(x, y, [knightX, knightY]) {
const isKnightHere = knightX === x && knightY === y;
return isKnightHere ? <Knight /> : null;
}
function Board({ knightPosition }) {
const squares = [];
for (let i = 0; i < 64; i++) {
squares.push(renderSquare(i, knightPosition));
}
return (
<DndProvider backend={HTML5Backend}>
<div style={boardStyle}>
{squares}
</div>
</DndProvider>
);
}
export default Board;
03 コンポーネントにオーバーしたときの処理
useDrop
フックが返す配列のはじめの要素は、collect()
関数により集められたプロパティのオブジェクトです。関数の引数オブジェクトに、コールバックcollect()
として定めてください。コールバックが受け取るmonitor
のisOver()
メソッドを呼び出すと、ドロップ先に重なっているかの論理値が得られます。
モジュールsrc/components/BoardSquare.js
をつぎのように書き替えれば、ドラッグで重ねたマス目が半透明の黄色になります。
src/components/BoardSquare.jsconst overlayStyle = { position: 'absolute', top: 0, left: 0, height: '100%', width: '100%', zIndex: 1, opacity: 0.5, backgroundColor: 'yellow', } const BoardSquare = ({ x, y, children }) => { // const [, drop] = useDrop(() => ({ const [{ isOver }, drop] = useDrop(() => ({ collect: monitor => ({ isOver: !!monitor.isOver(), }), }), [x, y]); return ( <div ref={drop} style={boardSquareStyle} > {isOver && (<div style={overlayStyle} />)} </div> ) }
04 ドロップできるマス目かどうか色分けする
ドラッグしているときのマス目の色を、3種類にしましょう。まず、ナイトをドロップできるマス目は黄色にします。オーバーしたマス目は2種類に分け、ドロップできなければ赤、できるなら青です。
- ドロップできる: 黄
- オーバーしてもドロップできない: 赤
- オーバーしてドロップできる: 青
マス目に重ねて色を変えるコンポーネントは、つぎのように新たなモジュール(src/components/Overlay.js
)に分けます(コード003)。引数に受け取るプロパティ(color
)が背景色(backgroundColor
)に与える色です。
コード003■マス目に重ねて色を変えるモジュール
src/components/Overlay.js
const overlayStyle = {
position: 'absolute',
top: 0,
left: 0,
height: '100%',
width: '100%',
zIndex: 1,
opacity: 0.5,
};
const Overlay = ({ color }) => {
return (
<div
style={{
...overlayStyle,
backgroundColor: color,
}}
/>
);
};
export default Overlay;
モジュールsrc/components/BoardSquare.js
は、マス目に色づけする要素を今つくったコンポーネントと差し替えます。また、Game
モジュールからimport
に加えるのが、ドロップしてよいマス目を調べる関数(canMoveKnight()
)です。また、useDrop
のcollect()
で、プロパティcanDrop
にmonitor
のcanDrop()
が返す値を与えてください。すると、仕様オブジェクトにコールバックcanDrop()
で定めた戻り値が、ドロップしてよいマス目かどうかを決める論理値として、useDrop
の戻り値から取り出せるのです。これで、マス目のカラーが3種類に塗り分けされます(図003)。
src/components/BoardSquare.js// import { moveKnight } from '../Game'; import { canMoveKnight, moveKnight } from '../Game'; import Overlay from './Overlay'; /* const overlayStyle = { position: 'absolute', top: 0, left: 0, height: '100%', width: '100%', zIndex: 1, opacity: 0.5, backgroundColor: 'yellow', }; */ const BoardSquare = ({ x, y, children }) => { // const [{ isOver }, drop] = useDrop(() => ({ const [{ isOver, canDrop }, drop] = useDrop(() => ({ canDrop: () => canMoveKnight(x, y), collect: monitor => ({ canDrop: !!monitor.canDrop(), }), }), [x, y]); return ( <div ref={drop} style={boardSquareStyle} > <Square black={black}>{children}</Square> {/* {isOver && (<div style={overlayStyle} />)} */} {isOver && !canDrop && <Overlay color="red" />} {!isOver && canDrop && <Overlay color="yellow" />} {isOver && canDrop && <Overlay color="green" />} </div> ); };
図003■ドロップできるマス目は黄色でドロップできないマス目にオーバーすると赤
ノート02■モジュールsrc/components/Overlay.js
「Tutorial」には、モジュールsrc/components/Overlay.js
の記述がありません。けれど、src/components/BoardSquare.js
のJSXにはOverlay
コンポーネントが差し込まれているので、記載漏れだと思われます(「Make the Board Squares Droppable」参照)。
モジュールsrc/components/BoardSquare.js
の記述全体は、つぎのコード004にまとめたとおりです。
コード004■ナイトの位置とマス目の処理をするモジュール
src/components/BoardSquare.js
import { useDrop } from 'react-dnd';
import { canMoveKnight, moveKnight } from '../Game';
import { ItemTypes } from '../ItemTypes';
import Square from './Square';
import Overlay from './Overlay';
const boardSquareStyle = {
position: 'relative',
width: '100%',
height: '100%',
};
const BoardSquare = ({ x, y, children }) => {
const black = (x + y) % 2 === 1;
const [{ isOver, canDrop }, drop] = useDrop(() => ({
accept: ItemTypes.KNIGHT,
drop: () => moveKnight(x, y),
canDrop: () => canMoveKnight(x, y),
collect: (monitor) => ({
isOver: !!monitor.isOver(),
canDrop: !!monitor.canDrop(),
}),
}), [x, y]);
return (
<div
ref={drop}
style={boardSquareStyle}
>
<Square black={black}>{children}</Square>
{isOver && !canDrop && <Overlay color="red" />}
{!isOver && canDrop && <Overlay color="yellow" />}
{isOver && canDrop && <Overlay color="green" />}
</div>
);
};
export default BoardSquare;
05 ドラッグしているときのイメージを変える
useDrag
が返す配列の3つ目の要素は、ドラッグのプレビュー表示とDOMを結ぶconnector()
関数です。React DnDのユーティリティコンポーネントDragPreviewImage
と組み合わせて、ドラッグしているときのイメージが定められます。イメージのSVGデータは、公式作例(knightImage.js
)からコピーしてsrc/components/knightImage.js
として保存してください。
モジュールsrc/components/Knight.js
のテンプレートにDragPreviewImage
コンポーネントを差し込み、useDrag
の戻り値から取り出した3つ目の要素(preview
)の関数をコンポーネントのconnect
に与えます。また、SVGデータのパスはsrc
に定めてください。
src/components/Knight.js
// import { useDrag } from 'react-dnd'
; import { DragPreviewImage, useDrag } from 'react-dnd'; import { knightImage } from './knightImage'; const Knight = () => { // const [{ isDragging }, drag] = useDrag(() => ({ const [{ isDragging }, drag, preview] = useDrag(() => ({ }), })); return ( <> <DragPreviewImage connect={preview} src={knightImage} /> <div > </div> </> ); };
これで、ナイトの駒をドラッグすると、その間のイメージが変わります(図004)。書き替えたナイトのモジュールのスクリプト全体は、以下のコード005のとおりです。なお、モジュールsrc/Game.js
とsrc/components/Square.js
のコードは、前回のまま手は加えていません。React DnDサイトの「Tutorial」作例ができ上がりました。CodeSandboxにはサンプル001を公開しましたので、各モジュールのコードと動きはこちらでお確かめください。
もっとも、公式「Tutorial」が公開している作例を見ると、コードがあちこち異なります。それどころか、この公開作例には「Tutorial」に解説されていないモジュールさえ含まれています。そこで次回は、今回書き上げたコードを公開作例に合わせて手直ししてみます。
図004■ドラッグ中のイメージが変わる
コード005■ドラッグ中のイメージが変わるナイトのモジュール
src/components/Knight.js
import { DragPreviewImage, useDrag } from 'react-dnd';
import { knightImage } from './knightImage';
import { ItemTypes } from '../ItemTypes';
const knightStyle = {
fontSize: 60,
fontWeight: 'bold',
textAlign: 'center',
lineHeight: '4rem',
cursor: 'move',
};
const Knight = () => {
const [{ isDragging }, drag, preview] = useDrag(() => ({
type: ItemTypes.KNIGHT,
collect: (monitor) => ({
isDragging: !!monitor.isDragging(),
}),
}));
return (
<>
<DragPreviewImage connect={preview} src={knightImage} />
<div
ref={drag}
style={{
...knightStyle,
opacity: isDragging ? 0.5 : 1,
}}
>
♘
</div>
</>
);
};
export default Knight;
サンプル001■React DnD 02: Chess board and lonely Knight
Create React App + React DnD
- Create React App + React DnD 01: ドラッグ&ドロップの前に ー クリックで動かす
- Create React App + React DnD 02: ドラッグ&ドロップで動かす
- Create React App + React DnD 03: アプリケーションを組み立て直す
- Create React App + React DnD 04: アプリケーションの処理を改善する
作成者: 野中文雄
更新日: 2021年06月08日 構文002に加筆・補正。
更新日; 2021年05月01日 本文の説明を一部追加・補正。
更新日: 2021年04月22日 React DnDの最新バージョンに合わせたコードと解説の改訂。
作成日: 2020年04月05日
Copyright © 2001-2020 Fumio Nonaka. All rights reserved.