LoginSignup
3
2

More than 3 years have passed since last update.

Reactのチュートリアルの三目並べを素のJavaScriptに

Last updated at Posted at 2020-03-25

前回の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;
    });
  });
})();

image.png

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;
    }
  });
})();

image.png

循環参照が起きると思ってクリック処理を分けてみました!
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;
    }
  });
})();

image.png

もはや循環参照とかわからんとです。
前回は「勝者:〇〇」って実装してなかったので、今回しました。

履歴機能を持たせる

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;
    }
  });
})();

image.png
手番はマス目の配列と一緒に保持するようにしました。
こっちの方が楽でした。

感想

jQueryの時のソースと、バグりまくった経験があったからか、割と早く作れた。むしろこっちの方が内容に無駄がないかも・・・?

でも相変わらず見にくい。

3
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2