前回のjQueryで作ったReactチュートリアルを素のJavaScriptにしてみます。すべてのブラウザで動くかは試してません。Edge(chromium)で確認しました。
#マス目に数値を表示する
index.html
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="css/index.css" />
<script src="js/index.js"></script>
</head>
<body>
<div id="root">
<div class="game">
<div class="gmae-board">
<div>
<div class="board-row">
<button class="square"></button><button class="square"></button
><button class="square"></button>
</div>
<div class="board-row">
<button class="square"></button><button class="square"></button
><button class="square"></button>
</div>
<div class="board-row">
<button class="square"></button><button class="square"></button
><button class="square"></button>
</div>
</div>
</div>
<div class="game-info">
<div>次の手番: X</div>
<div>
<li><button>Go to game start</button></li>
<!-- <li><button>Go to move #1</button></li> -->
</div>
</div>
</div>
</div>
</body>
</html>
index.css
body {
font: 14px 'Century Gothic', Futura, sans-serif;
margin: 20px;
}
ol,
ul {
padding-left: 30px;
}
.board-row:after {
clear: both;
content: '';
display: table;
}
.status {
margin-bottom: 10px;
}
.square {
background: #fff;
border: 1px solid #999;
float: left;
font-size: 24px;
font-weight: bold;
line-height: 34px;
height: 34px;
margin-right: -1px;
margin-top: -1px;
padding: 0;
text-align: center;
width: 34px;
}
.square:focus {
outline: none;
}
.kbd-navigation .square:focus {
background: #ddd;
}
.game {
display: flex;
flex-direction: row;
}
.game-info {
margin-left: 20px;
}
index.js
(function() {
// DOM読み込み後
window.addEventListener("DOMContentLoaded", () => {
document.querySelectorAll(".square").forEach((element, index) => {
element.textContent = index;
});
});
})();
querySelectorAll
がなかった時代は、getElementsByClassName
で取得してforとかでぐるぐる回していたんでしょうね。
#XとOを入力できるようにする
index.js
(function() {
// DOM読み込み後
window.addEventListener("DOMContentLoaded", () => {
let xIsNext = true;
const status = document.querySelector(".game-info :first-child"); // つなげて書くとNGみたい
document.querySelectorAll(".square").forEach((element, index) => {
// クリックイベント
element.addEventListener("click", squareClick);
});
function squareClick(event) {
event.target.textContent = xIsNext ? "X" : "O";
status.textContent = "次の手番: " + (xIsNext ? "O" : "X");
xIsNext = !xIsNext;
}
});
})();
循環参照が起きると思ってクリック処理を分けてみました!
MDNのメモリ管理には、「もはや問題ではありません」と書かれているけど、どうなんだろ・・・。
#履歴なしの完成までもっていく
index.js
(function() {
// DOM読み込み後
window.addEventListener("DOMContentLoaded", () => {
const squares = new Array(9).fill(null);
let xIsNext = true;
const status = document.querySelector(".game-info :first-child"); // つなげて書くとNGみたい
document.querySelectorAll(".square").forEach((element, index) => {
// クリックイベント
element.addEventListener("click", squareClick.bind(null, index));
});
function squareClick(index, event) {
if (calculateWinner(squares) || squares[index]) {
return;
}
squares[index] = xIsNext ? "X" : "O";
event.target.textContent = squares[index];
// 勝利判定
if (calculateWinner(squares)) {
status.textContent = "勝者: " + squares[index];
} else {
status.textContent = "次の手番: " + (xIsNext ? "O" : "X");
}
xIsNext = !xIsNext;
}
// 勝敗判定関数(公式チュートリアルから拝借)
function calculateWinner(squares) {
const lines = [
[0, 1, 2],
[3, 4, 5],
[6, 7, 8],
[0, 3, 6],
[1, 4, 7],
[2, 5, 8],
[0, 4, 8],
[2, 4, 6]
];
for (let i = 0; i < lines.length; i++) {
const [a, b, c] = lines[i];
if (
squares[a] &&
squares[a] === squares[b] &&
squares[a] === squares[c]
) {
return squares[a];
}
}
return null;
}
});
})();
もはや循環参照とかわからんとです。
※前回は「勝者:〇〇」って実装してなかったので、今回しました。
#履歴機能を持たせる
cssとhtmlは最初と一緒です。
index.js
(function() {
// DOM読み込み後
window.addEventListener("DOMContentLoaded", () => {
const history = [
{ squares: new Array(9).fill(null), nextStatus: "次の手番: X" } // 履歴
];
let stepNumber = 0; // 現在表示している履歴のインデックス
const status = document.querySelector(".game-info :first-child"); // つなげて書くとNGみたい
// Go to game start ボタン
document
.querySelector(".game-info li > button")
.addEventListener("click", historyButtonClick.bind(null, 0));
// 全square
document.querySelectorAll(".square").forEach((element, index) => {
// クリックイベント
element.addEventListener("click", squareClick.bind(null, index));
});
function squareClick(index, event) {
// 現在のhistory
const current = history[stepNumber];
const squares = current.squares.concat(); // コピー
if (calculateWinner(squares) || squares[index]) {
return;
}
squares[index] = stepNumber % 2 === 0 ? "X" : "O";
event.target.textContent = squares[index];
// 勝利判定
if (calculateWinner(squares)) {
status.textContent = "勝者: " + squares[index];
} else {
status.textContent = "次の手番: " + (stepNumber % 2 === 0 ? "O" : "X");
}
// 現在のstemNumberより後ろの履歴と履歴ボタン削除
const lis = document.querySelectorAll(".game-info li");
const removeCount = history.length - (stepNumber + 1);
for (let i = 1; i <= removeCount; i++) {
lis[stepNumber + i].parentNode.removeChild(lis[stepNumber + i]);
history.pop();
}
// 新しい要素追加
history.push({ squares: squares, nextStatus: status.textContent });
stepNumber++;
createHistoryButton(stepNumber);
}
function createHistoryButton(index) {
// 履歴ボタン
const button = document.createElement("button");
button.textContent = "Go to move #" + index;
button.addEventListener("click", historyButtonClick.bind(null, index)); // クリックイベント
// 履歴ボタンの親
const li = document.createElement("li");
li.appendChild(button); // 履歴ボタンを追加
// liの親に追加
document.querySelector(".game-info").appendChild(li);
}
function historyButtonClick(index) {
stepNumber = index;
// 手番
status.textContent = history[stepNumber].nextStatus;
// マス目を全て上書く
const domSquares = document.querySelectorAll(".square");
history[stepNumber].squares.forEach((value, index) => {
domSquares[index].textContent = value;
});
}
// 勝敗判定関数(公式チュートリアルから拝借)
function calculateWinner(squares) {
const lines = [
[0, 1, 2],
[3, 4, 5],
[6, 7, 8],
[0, 3, 6],
[1, 4, 7],
[2, 5, 8],
[0, 4, 8],
[2, 4, 6]
];
for (let i = 0; i < lines.length; i++) {
const [a, b, c] = lines[i];
if (
squares[a] &&
squares[a] === squares[b] &&
squares[a] === squares[c]
) {
return squares[a];
}
}
return null;
}
});
})();
手番はマス目の配列と一緒に保持するようにしました。
こっちの方が楽でした。
#感想
jQueryの時のソースと、バグりまくった経験があったからか、割と早く作れた。むしろこっちの方が内容に無駄がないかも・・・?
でも相変わらず見にくい。