サイトトップ

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

HTML5テクニカルノート

Create React App + React DnD 01: ドラッグ&ドロップの前に ー クリックで動かす


React DnDは、ドラッグ&ドロップのインタフェースをコンポーネントから分けて処理できる、Reactのユーティリティです。React DnDのサイトには「Tutorial」があり、解説にしたがってコードを書き進めることにより、理解できる内容になっています。ただ、一部コードが抜けていたり、正しく動かないところがありました。そこで、同じ作例をコードは少し修正したうえで、順を追って解説することにします。今回は、ドラッグ&ドロップの前の準備です。クリックでオブジェクトを動かします。

01 チェスのナイトの駒をページに加える

Create React AppでつくったReactアプリケーションのひな型に、手を加えてゆくことにしましょう(ひな型アプリケーションのつくり方については、「Create React App 入門 01: 3×3のマス目をつくる」01「Reactアプリケーションのひな形をつくる」をお読みください)。まず、新たなモジュールsrc\components\Knight.jsをつくり、テンプレートにチェスのナイト、将棋でいえば桂馬を加えます(コード001)。 この駒が動かす対象です。

コード001■ナイトの駒のモジュール

src/components/Knight.js

import React from 'react';

const knightStyle = {
	fontSize: 40,
	fontWeight: 'bold',
	cursor: 'move',
};
const Knight = () => {
	return (
		<div style={knightStyle}>
			♘
		</div>
	);
}

export default Knight;

ひな型のモジュールsrc\index.jsは、つぎのように書き替えて、ナイトのコンポーネント(Knight)を差し込んでください。 ページにナイトが示されます(図001)。src/index.jssrc/App.jssrc/App.cssは使わないので削除して構いません。

src/index.js

const Board = (props) => {
import React from 'react';
import ReactDOM from 'react-dom';
// import './index.css';
// import App from './App';
import Knight from './components/Knight';
// import * as serviceWorker from './serviceWorker';

// ReactDOM.render(<App />, document.getElementById('root'));
ReactDOM.render(<Knight />, document.getElementById('root'));

// serviceWorker.unregister();

図001■ページにナイトが表れる

図001

02 盤のマス目をつくる

つぎにつくるのは、チェス盤(ボード)のマス目のモジュールsrc\components\Square.jsです(コード002)。親モジュール(src\index.js)から関数コンポーネント(Square)の引数に受け取るプロパティがふたつあります。blackは、マス目を白と黒に塗り分けるための論理値です。あとに示すとおり、ナイトのコンポーネントを子に加えるためプロパティchildrenも取り出します。

コード002■マス目のモジュール

src/components/Square.js

import React from 'react';

const squareStyle = {
	width: '100%',
	height: '100%',
};
const Square = ({ black, children }) => {
	const fill = black ? 'black' : 'white';
	const stroke = black ? 'white' : 'black';
	return (
		<div
			style={{
				...squareStyle,
				backgroundColor: fill,
				color: stroke,
			}}
		>
			{children}
		</div>
	);
}

export default Square;

モジュールsrc\index.jsは、コンポーネントSquareに白黒カラーの論理値をプロパティ(black)に与え、Knightコンポーネントは子として差し込みます。 マス目の白黒切り替えを確かめるため、つぎのコードではプロパティ値をtrueにしてみました(図002)。

src/index.js

// import Knight from './components/Knight';
import Square from './components/Square';

ReactDOM.render(
	// <Knight />,
	<Square black={true}>
		<Knight />
	</Square>,
	document.getElementById('root')
);

図002■マス目の背景色が黒になった

図002

03 チェス盤をつくる

さらに、チェス盤のモジュールsrc\components\Board.jsをつくります。まだ、マス目(Square)はひとつだけです(白黒のプロパティblackは一旦外しました)。

src/components/Board.js

import React from 'react';
import Square from './Square'
import Knight from './Knight'

function Board() {
	return (
		<div>
			<Square>
				<Knight />
			</Square>
		</div>
	);
}

export default Board;

マス目(Square)と駒(Knight)のコンポーネントは盤のコンポーネント(Board)に移したので、モジュールsrc\index.jsはこれらを盤に差し替えるだけです。配列を与えたプロパティ(knightPosition)は、あとでナイトを置くマス目の位置決めに使います。

src/index.js

// import Square from './components/Square';
// import Knight from './components/Knight';
import Board from './components/Board';

ReactDOM.render(
	/* <Square black={true}>
		<Knight />
	</Square>, */
	<Board knightPosition={[0, 0]} />,
	document.getElementById('root')
);

04 8×8のマス目を置く

いよいよ、8×8のチェス盤のマス目を置いていきます。白と黒を交互に与えなければなりません。つぎのコードが、チェス盤のモジュールsrc\components\Board.jsに加える修正です。マス目のテンプレートを返す関数(renderSquare())が引数([knightX, knightY])から取り出したナイトの位置情報と合致した(isKnightHere)マス目に、ナイト(Knight)のコンポーネントが置かれます。

src/components/Board.js

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);
	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>
		</div>
	);
}
// function Board() {
function Board({ knightPosition }) {
	return (
		// <div>
		<div style={boardStyle}>
			{/* <Square>
				<Knight />
			</Square> */}
			{renderSquare(0, knightPosition)}
			{renderSquare(1, knightPosition)}
			{renderSquare(2, knightPosition)}
		</div>
	);
}

export default Board;

試しに、盤のコンポーネントに加えた位置情報配列(knightPosition)の値を変えると、ナイトの駒が移動します(図003)。

src/index.js

ReactDOM.render(
	<Board knightPosition={[2, 0]} />,
	document.getElementById('root')
);

図003■ナイトの位置が変わる

図003

確認ができたので、改めてfor文で、つぎのように8×8の盤面をつくりましょう(図004)。配列(squares)に収めたテンプレート要素は、順に取り出されて差し込まれます。

src/components/Board.js

function Board({ knightPosition }) {
	const squares = []
	for (let i = 0; i < 64; i++) {
		squares.push(renderSquare(i, knightPosition))
	}
	return (
		<div style={boardStyle}>
			{/* {renderSquare(0, knightPosition)}
			{renderSquare(1, knightPosition)}
			{renderSquare(2, knightPosition)} */}
			{squares}
		</div>
	);
}

図004■8×8の盤面が描かれた

図004

05 ナイトをマス目のクリックで動かす

ナイトをドラッグで動かす前段階として、クリックしたマス目に配置できるようにします。すると、ナイトが盤面の8×8のどのマス目にあるのか、わかっていなければなりません。新たに定めるsrc\Game.jsが、そのためのモジュールです。

位置情報は盤のコンポーネント(Board)に与えたのと同じ配列のかたちで変数(knightPosition)にもたせました。モジュールにはその位置を変える関数(moveKnight())が備わります。問題はそのときどのようにして、Reactに再描画すべきことを伝えるかです。そこで、再描画のためのコールバック関数(observer())を別に受け取っておくことにします(関数observe())。そして、位置が変わったとき、それを伝える関数(emitChange())がコールバックを呼び出せばよいのです。新たな位置は、引数として渡されます。

src/Game.js

let knightPosition = [0, 0];
let observer = null;
function emitChange() {
	observer(knightPosition)
}
export function observe(o) {
	if (observer) {
		throw new Error('Multiple observers not implemented.');
	}
	observer = o;
	emitChange();
}
export function moveKnight(toX, toY) {
	knightPosition = [toX, toY];
	emitChange();
}

描画のためのコールバックは、モジュールsrc\index.jsからobserve()関数でつぎのように渡します。コールバックは盤面のコンポーネント(Board)にナイトの位置情報(knightPosition)を渡すとともに、ReactDOM.render()メソッドにより再描画するのです。

src/index.js

import { observe } from './Game';

/* ReactDOM.render(
	<Board knightPosition={[2, 0]} />,
	document.getElementById('root')
); */
const root = document.getElementById('root')
observe(knightPosition =>
	ReactDOM.render(<Board knightPosition={knightPosition} />,
	root)
);

onClickハンドラを加えるのは、モジュールsrc\components\Board.jsが定めるマス目のテンプレートを返す関数(renderSquare())です。ハンドラが実行する関数(handleSquareClick())から、Gameモジュールの関数(moveKnight())を呼び出しすことによりナイトの位置が動きます。

src/components/Board.js

import { moveKnight } from '../Game';

function renderSquare(i, [knightX, knightY]) {

	return (
		<div
			key={i}
			style={squareStyle}
			onClick={() => handleSquareClick(x, y)}
		>

		</div>
	);
}
function handleSquareClick(toX, toY) {
	moveKnight(toX, toY);
}

06 ナイトが行ける先をルール決めする

ナイトの一手で行ける先には決まりがあります。水平・垂直のどちらかにひとマス、もう一方にふたマスの位置です。モジュールsrc\Game.jsに、その判定の関数(canMoveKnight())をつぎのように加えましょう。

src/Game.js

let knightPosition = [1, 7];  // [0, 0];

export function canMoveKnight(toX, toY) {
	const [x, y] = knightPosition
	const dx = toX - x
	const dy = toY - y
	return (
			(Math.abs(dx) === 2 && Math.abs(dy) === 1) ||
			(Math.abs(dx) === 1 && Math.abs(dy) === 2)
	)
}

モジュールsrc\components\Board.jsは、この行先判定の関数(canMoveKnight())をimportし、クリックハンドラ関数(handleSquareClick())は正しいマス目にだけ動けるように書き替えます。

src/components/Board.js

// import { moveKnight } from '../Game';
import { canMoveKnight, moveKnight } from '../Game'

function handleSquareClick(toX, toY) {
	if (canMoveKnight(toX, toY)) {
		moveKnight(toX, toY);
	}
}

クリックして移れるマス目が、ナイトのルールにしたがった先になりました。今回はここまでです。手を加えた3つのモジュールのコードをつぎに掲げます(コード003)。併せて、以下のサンプル001にCodeSandoboxのコードを公開しました。次回は、いよいよReact DnDを使ったドラッグ&ドロップの実装です。

コード003■盤面のマス目のクリックでナイトの駒を移動する

src/index.js

import React from 'react';
import ReactDOM from 'react-dom';
import { observe } from './Game';
import Board from './components/Board';

const root = document.getElementById('root')
observe(knightPosition =>
	ReactDOM.render(<Board knightPosition={knightPosition} />,
	root)
);

src/Game.js

let knightPosition = [1, 7];
let observer = null;
function emitChange() {
	observer(knightPosition)
}
export function observe(o) {
	if (observer) {
		throw new Error('Multiple observers not implemented.');
	}
	observer = o;
	emitChange();
}
export function moveKnight(toX, toY) {
	knightPosition = [toX, toY];
	emitChange();
}
export function canMoveKnight(toX, toY) {
	const [x, y] = knightPosition
	const dx = toX - x
	const dy = toY - y
	return (
			(Math.abs(dx) === 2 && Math.abs(dy) === 1) ||
			(Math.abs(dx) === 1 && Math.abs(dy) === 2)
	)
}

src/components/Board.js

import React from 'react';
import { canMoveKnight, moveKnight } from '../Game'
import Square from './Square'
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);
	const black = (x + y) % 2 === 1;
	const isKnightHere = knightX === x && knightY === y;
	const piece = isKnightHere ? <Knight /> : null;
	return (
		<div
			key={i}
			style={squareStyle}
			onClick={() => handleSquareClick(x, y)}
		>
			<Square black={black}>{piece}</Square>
		</div>
	);
}
function Board({ knightPosition }) {
	const squares = []
	for (let i = 0; i < 64; i++) {
		squares.push(renderSquare(i, knightPosition))
	}
	return (
		<div style={boardStyle}>
			{squares}
		</div>
	);
}
function handleSquareClick(toX, toY) {
	if (canMoveKnight(toX, toY)) {
		moveKnight(toX, toY);
	}
}

export default Board;

サンプル001■React DnD 01: Chess board and lonely Knight

Create React App + React DnD


作成者: 野中文雄
作成日: 2020年03月15日


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