サイトトップ

Director Flash 書籍 業務内容 プロフィール

HTML5テクニカルノート

Create React App + React DnD 02: ドラッグ&ドロップで動かす


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コンポーネントです。バックエンド(Backend)は、backendプロパティに定めてください。クリックイベントハンドラは、もう外してしまいます。

src\components\Board.js

import { DndProvider } from 'react-dnd'
import Backend from 'react-dnd-html5-backend'

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={Backend}>
			<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)を使います。引数はふたつで、itemオブジェクトとcollect()関数です。itemオブジェクトには、typeプロパティのみ加えました。関数collect()は、引数のmonitorからドラッグするオブジェクトの状態として必要なプロパティを取り出し、オブジェクトにして返します。

useDrag()関数の戻り値は配列です。はじめの要素はcollect()関数の集めたプロパティが収められたオブジェクトです。isDragging()メソッドは、ドラッグ中かどうかを論理値で返します[*1]。ふたつ目の要素(drag)がドラッグしているオブジェクトとDOMを結ぶconnector()関数です。この関数は、テンプレートのref属性に与えてください。

src\components\Knight.js

import { useDrag } from 'react-dnd'
import { ItemTypes } from '../ItemTypes'

const Knight = () => {
	const [{ isDragging }, drag] = useDrag({
		item: { type: ItemTypes.KNIGHT },
		collect: monitor => ({
			isDragging: !!monitor.isDragging(),
		}),
	})
	return (
		<div
			ref={drag}
			style={{
				...knightStyle,
				opacity: isDragging ? 0.5 : 1,
			}}
		>
			♘
		</div>
	)
}

[*1] 論理否定演算子!をふたつ添えると、truetruefalsefalseのまま変わりません。けれど、オペランドがブール(論理)値でないとき、ひとつめの演算子でブール値評価されたうえで否定(反転)されます。つまり、結果を必ずブール値にすることが目的です。

これでドラッグはできるものの、ドロップできません(図001)。マウスボタンを放せば、もとの位置に戻ってしまいます。つぎにドロップ先を決めなければならないのです。useDrag()関数については、つぎに簡単にまとめておきます。引数などは、今回扱う範囲でご紹介しました。詳しくは、公式サイトのuseDrag()をお読みください。

表001■useDrag()関数

useDrag()
引数 ドラッグの設定を示すオブジェクト。
  • item: ドラッグのデータを表すオブジェクト(必須)
    • type: ドロップ先を区別する文字列またはシンボル(必須)。
    • 任意: あとはidくらいにしてシンプルに。
  • collect: コンポーネントに注入されるドラッグプロパティのオブジェクトを返す関数。
    • monitor: collect関数が受け取る引数。ドラッグされているコンポーネントの状態を監視するオブジェクト。
戻り値 ドラッグの内容を表す配列。
  1. collect()関数が集めたプロパティを収めるオブジェクト。
  2. ドラッグのソースオブジェクトとDOMを結ぶconnector()関数
  3. ドラッグのプレビュー表示とDOMを結ぶconnector()関数

図001■ドラッグできてもドロップするともとの位置に戻ってしまう

図001

02 コンポーネントをドロップする

ナイトを動かすときのマス目の位置情報は、これまで盤面のコンポーネント(Board)が担ってきました。ドラッグ&ドロップを扱おうとすると、コードが込み入ってきます。そこで、盤面から位置情報にもとづく処理を切り分けましょう。

もっとも、位置情報の処理をマス目(Square)に移したのでは、切り分けたことになりません。マス目を包む新たな位置情報のコンポーネント(BoardSquare)を、このあとつくることにします。

src\components\Board.js

// import { canMoveKnight, moveKnight } from '../Game'
// 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()関数です。テンプレートのref属性に与えてください。

新たなモジュールsrc\components\BoardSquare.jsの定めは、つぎのとおりです。これで、ナイトがドラッグ&ドロップできるようになります。ただし、ドロップ先が適切かどうかは判定していません。そのため、どのマス目にもドロップできます(図002)。

src\components\BoardSquare.js

import 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),
	})
	return (
		<div
			ref={drop}
			style={boardSquareStyle}
		>
			<Square black={black}>{children}</Square>
		</div>
	)
}
export default BoardSquare

この機会に、ナイトの駒のスタイルも少し整えましょう。

src/components/Knight.js

const knightStyle = {
	fontSize: 60,  /* 40, */

	textAlign: "center",
	lineHeight: '4rem',

};

図002■盤面のどのマス目にもドロップできる

図002

useDrop()関数について、つぎに簡単にまとめておきます。引数などは、今回扱う範囲でご紹介しました。詳しくは、公式サイトのuseDrop()をお読みください。盤面のモジュールのスクリプト全体は、以下のコード002のとおりです。

表002■useDrop()関数

useDrop()
引数 ドラッグの設定を示すオブジェクト。
  • accept: ドロップ可能なソースオブジェクトを示す値(必須)
    • type: ドロップ先を区別する文字列またはシンボル(必須)。
    • 任意: あとはidくらいにしてシンプルに。
  • collect: コンポーネントに注入されるドラッグプロパティのオブジェクトを返す関数。
    • monitor: collect関数が受け取る引数。ドラッグされているコンポーネントの状態を監視するオブジェクト。
戻り値 ドラッグの内容を表す配列。
  1. collect()関数が集めたプロパティを収めるオブジェクト。
  2. ドラッグのソースオブジェクトとDOMを結ぶconnector()関数
  3. ドラッグのプレビュー表示とDOMを結ぶconnector()関数

コード002■盤面のモジュール

src/components/Board.js

import React from 'react';
import { DndProvider } from 'react-dnd'
import Backend 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={Backend}>
			<div style={boardStyle}>
				{squares}
			</div>
		</DndProvider>
	);
}
export default Board;

03 コンポーネントにオーバーしたときの処理

useDrop()関数が返す配列のはじめの要素は、collect()関数により集められたプロパティのオブジェクトです。関数の引数オブジェクトに、コールバックcollect()として定めてください。コールバックが受け取るmonitorからは、ドロップ先に重なっているかを調べるisOver()が得られます。

モジュールsrc\components\BoardSquare.jsをつぎのように書き替えれば、ドラッグで重ねたマス目が半透明の黄色になります。

src\components\BoardSquare.js

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 [, drop] = useDrop({
	const [{ isOver }, drop] = useDrop({

		collect: monitor => ({
			isOver: !!monitor.isOver(),
		}),
	})
	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

import React from 'react'
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()で、monitorからcanDrop()を取得してください。そして、コールバックから、ドロップしてよいマス目か調べる関数を呼び出せばよいのです。これで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(),
		}),
	})
	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■ドロップできるマス目は黄色でドロップできないマス目にオーバーすると赤

図003

コード004■ナイトの位置にともなう処理をするモジュール

src\components\BoardSquare.js

import React from 'react';
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(),
		}),
	})
	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.jssrc/components/Square.jsのコードは、前回のまま手は加えていません。React DnDサイトの「Tutorial」作例ができ上がりました。CodeSandboxにはサンプル001を公開しましたので、各モジュールのコードと動きはこちらでお確かめください。

もっとも、公式「Tutorial」が公開している作例を見ると、コードがあちこち異なります。それどころか、この公開作例には「Tutorial」に解説されていないモジュールさえ含まれています。そこで次回は、今回書き上げたコードを公開作例に合わせて手直ししてみます。

図004■ドラッグ中のイメージが変わる

図004

コード005■ドラッグ中のイメージが変わるナイトのモジュール

src/components/Knight.js

import React from 'react';
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({
		item: { 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


作成者: 野中文雄
作成日: 2020年04月05日


Copyright © 2001-2020 Fumio Nonaka.  All rights reserved.