HTML5テクニカルノート
Create React App + React DnD 01: ドラッグ&ドロップの前に ー クリックで動かす
- ID: FN2003001
- Technique: ECMAScript 2015
- Library: React 17.0.2 / React DnD 14.0.2
React DnDは、ドラッグ&ドロップのインタフェースをコンポーネントから分けて処理できる、Reactのユーティリティです。React DnDのサイトには「Tutorial」があり、解説にしたがってコードを書き進めることにより、理解できる内容になっています。ただ、一部コードが抜けていたり、正しく動かないところがありました。そこで、同じ作例をコードは少し修正したうえで、順を追って解説することにします。今回は、ドラッグ&ドロップの前の準備です。クリックでオブジェクトを動かします。
01 チェスのナイトの駒をページに加える
Create React AppでつくったReactアプリケーションのひな型に、手を加えてゆくことにしましょう(ひな型アプリケーションのつくり方については、「Create React App 入門 01: 3×3マスのゲーム盤をつくる」01「Reactアプリケーションのひな形をつくる」をお読みください)。React DnDは、今回はまだ使いません。インストールは次回行うことにします。
まず、新たなモジュールsrc/components/Knight.js
をつくり、テンプレートにチェスのナイト♘、将棋でいえば桂馬を加えます(コード001)。 この駒が動かす対象です。
コード001■ナイトの駒のモジュール
src/components/Knight.js
const knightStyle = {
fontSize: 40,
fontWeight: 'bold',
cursor: 'move',
};
const Knight = () => {
return (
<div style={knightStyle}>
♘
</div>
);
};
export default Knight;
ひな型のモジュールsrc/index.js
は、つぎのように書き替えて、ナイトのコンポーネント(Knight
)を差し込んでください。 ページにナイトが示されます(図001)。src/App.js
やsrc/App.css
は使わないので削除して構いません。
src/index.js// import './index.css'; // import App from './App'; import Knight from './components/Knight'; ReactDOM.render( <React.StrictMode> {/* <App /> */} <Knight /> </React.StrictMode>, document.getElementById('root') );
図001■ページにナイトが表れる
02 盤のマス目をつくる
つぎにつくるのは、チェス盤(ボード)のマス目のモジュールsrc/components/Square.js
です(コード002)。親モジュール(src/index.js
)から関数コンポーネント(Square
)の引数に受け取るプロパティがふたつあります。black
は、マス目を白と黒に塗り分けるための論理値です。あとに示すとおり、ナイトのコンポーネントを子に加えるためプロパティchildren
も取り出します。
コード002■マス目のモジュール
src/components/Square.js
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( <React.StrictMode> <Square black={true}> <Knight /> </Square> </React.StrictMode>, document.getElementById('root') );
図002■マス目の背景色が黒になった
03 チェス盤をつくる
さらに、チェス盤のモジュールsrc/components/Board.js
をつくります。まだ、マス目(Square
)はひとつだけです(白黒のプロパティblack
は一旦外しました)。
src/components/Board.jsimport 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( <React.StrictMode> /* <Square black={true}> <Knight /> </Square>, */ <Board knightPosition={[0, 0]} /> </React.StrictMode>, document.getElementById('root') );
04 8×8のマス目を置く
いよいよ、8×8のチェス盤のマス目を置いていきます。白と黒を交互に与えなければなりません。つぎのコードが、チェス盤のモジュールsrc/components/Board.js
に加える修正です。マス目のテンプレートを返す関数(renderSquare()
)が引数([knightX, knightY]
)から取り出したナイトの位置情報と合致した(isKnightHere
)マス目に、ナイト(Knight
)のコンポーネントが置かれます。
src/components/Board.jsconst 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.jsReactDOM.render( <React.StrictMode> <Board knightPosition={[2, 0]} /> </React.StrictMode>, document.getElementById('root') );
図003■ナイトの位置が変わる
確認ができたので、改めてfor
文で、つぎのように8×8の盤面をつくりましょう(図004)。配列(squares
)に収めたテンプレート要素は、順に取り出されて差し込まれます。
src/components/Board.jsfunction 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の盤面が描かれた
05 ナイトをマス目のクリックで動かす
ナイトをドラッグで動かす前段階として、クリックしたマス目に配置できるようにします。すると、ナイトが盤面の8×8のどのマス目にあるのか、わかっていなければなりません。新たに定めるsrc/Game.js
が、そのためのモジュールです。
位置情報は盤のコンポーネント(Board
)に与えたのと同じ配列のかたちで変数(knightPosition
)にもたせました。モジュールにはその位置を変える関数(moveKnight()
)が備わります。問題はそのときどのようにして、Reactに再描画すべきことを伝えるかです。そこで、再描画のためのコールバック関数(observer()
)を別に受け取っておくことにします(関数observe()
)。そして、位置が変わったとき、それを伝える関数(emitChange()
)がコールバックを呼び出せばよいのです。新たな位置は、引数として渡されます。
src/Game.jslet 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.jsimport { observe } from './Game'; /* ReactDOM.render( <React.StrictMode> <Board knightPosition={[2, 0]} /> </React.StrictMode>, document.getElementById('root') ); */ const root = document.getElementById('root'); observe((knightPosition) => ReactDOM.render( <React.StrictMode> <Board knightPosition={knightPosition} /> </React.StrictMode>, root) );
onClick
ハンドラを加えるのは、モジュールsrc/components/Board.js
が定めるマス目のテンプレートを返す関数(renderSquare()
)です。ハンドラが実行する関数(handleSquareClick()
)から、Game
モジュールの関数(moveKnight()
)を呼び出しすことによりナイトの位置が動きます。
src/components/Board.jsimport { 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.jslet 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(
<React.StrictMode>
<Board knightPosition={knightPosition} />
</React.StrictMode>,
root)
);
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)
);
}
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
- 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年04月22日 誤りの訂正と補足、およびバージョン更新にともなうコード修正。
作成日: 2020年03月15日
Copyright © 2001-2020 Fumio Nonaka. All rights reserved.