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

[前回](https://qiita.com/GodPhwng/items/168576c0a9c89375a3f0)のjQueryで作ったReactチュートリアルを素のJavaScriptにしてみます。すべてのブラウザで動くかは試してません。Edge(chromium)で確認しました。

#マス目に数値を表示する
```html: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>
```

```css: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;
}
```

```javascript:index.js
(function() {
  // DOM読み込み後
  window.addEventListener("DOMContentLoaded", () => {
    document.querySelectorAll(".square").forEach((element, index) => {
      element.textContent = index;
    });
  });
})();
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/526982/29bfefdb-7745-1a58-9f4a-5db01a116928.png)

`querySelectorAll`がなかった時代は、`getElementsByClassName`で取得してforとかでぐるぐる回していたんでしょうね。

#XとOを入力できるようにする

```javascript: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/526982/1e8f2d07-b4ff-e631-4e5d-68cad19e0ccb.png)

循環参照が起きると思ってクリック処理を分けてみました！
MDNの[メモリ管理](https://developer.mozilla.org/ja/docs/Web/JavaScript/Memory_Management)には、「もはや問題ではありません」と書かれているけど、どうなんだろ・・・。

#履歴なしの完成までもっていく

```javascript: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/526982/6fbb262a-a064-27a1-fa2f-027fa83d98db.png)

もはや循環参照とかわからんとです。
※[前回](https://qiita.com/GodPhwng/items/168576c0a9c89375a3f0)は「勝者：〇〇」って実装してなかったので、今回しました。

#履歴機能を持たせる
cssとhtmlは最初と一緒です。

```javascript: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/526982/2e4b60b5-7520-57a0-4a5d-8dead2520016.png)
手番はマス目の配列と一緒に保持するようにしました。
こっちの方が楽でした。

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

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