ReactチュートリアルをVue.jsで実装

Vue.jsとReactの勉強のため，[Reactのチュートリアル](https://ja.reactjs.org/tutorial/tutorial.html)の「Tic Tac Toe」をVue.jsで実装してみる。

vue-cliを使ってプロジェクトを作成（省略）。
バージョンはVue2。

`components/`フォルダ以下に次のファイルを作成。

- Square.vue
    - ゲームの1マスを管理
- Board.vue
    - ゲームボード（9マス）を管理
- Game.vue
    - ゲーム全体の状態（手番，履歴）を管理

ちなみにcssもチュートリアルと同じになるように移植した。

# Square.vue

```html:Square.vue
<template>
  <button class="square" @click="onClick">{{value}}</button>
</template>

<style scoped>
.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;
  background: #ddd;
}
</style>

<script>
export default {
  name: 'Square',
  props: {
    value: String,
    onClick: Function,
  },
}
</script>
```

Squareコンポーネントはゲームの1マスにあたるボタンを表示する。
同時にボタンクリックのイベントを発火してゲームが進行するのだが，ゲームの状態は上位のGameコンポーネントが管理している。
通常はemitを使うところなのだろうが，ここでは`button@click`に上位コンポーネントからプロパティとして引き渡された`onClick`（名前は何でもよい）関数を当ててみたら上手く動いた。（この方法が正しいのかどうか，どなたかご教示ください。）

# Board.vue

```html:Board.vue
<template>
  <div>
    <div class="board-row" v-for="r in [0,1,2]" :key="r">
      <square v-for="c in [0,1,2]" :key="c"
        :value="squares[r*3+c]"
        :onClick="() => onClick(r*3+c)"
      />
    </div>
  </div>
</template>

<style scoped>
.board-row:after {
  clear: both;
  content: "";
  display: table;
}
.status {
  margin-bottom: 10px;
}
</style>

<script>
import Square from '@/components/Square'

export default {
  name: 'Board',
  components: {
    Square,
  },
  props: {
    squares: Array,
    onClick: Function,
  },
}
</script>
```

Boardコンポーネントはゲームの盤面である9つのマスを管理する。
Reactのチュートリアルでは，Square 1つのレンダリングを関数にして，それを9回呼び出す方式をとっている。
Reactではレンダリングのテンプレート自体がJavaScript（の拡張）なのでそういったことができるが，Vue.jsで相当する方法が判らなかったので，ここでは`v-for`による二重ループで実装した。
`onClick`は「上から渡されたハンドラ関数をマス番号の引数付きで呼び出す」アロー関数に設定する。

# Game.vue

```html:Game.vue
<template>
  <div class="game">
    <div class="game-board">
      <board
        :squares="current.squares"
        :onClick="handleClick"
      />
    </div>
    <div class="game-info">
      <div>{{status}}</div>
      <ol>
        <li v-for="(step, move) in history" :key="move">
          <button @click="() => jumpTo(move)">{{move ? 'Go to move #'+move : 'Go to game start'}}</button>
        </li>
      </ol>
    </div>
  </div>
</template>

<style scoped>
.game {
  display: flex;
  flex-direction: row;
}
.game-info {
  margin-left: 20px;
}
ol, ul {
  padding-left: 30px;
}
</style>

<script>
import Board from '@/components/Board'

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
}

export default {
  name: 'Game',
  components: {
    Board,
  },
  data() {
    return {
      history: [{
        squares: Array(9).fill(null)
      }],
      stepNumber: 0,
      xIsNext: true,
    }
  },
  computed: {
    current() {
      return this.history[this.stepNumber]
    },
    winner() {
      return calculateWinner(this.current.squares)
    },
    status() {
      return this.winner ? 'Winner: ' + this.winner : 'Next player: ' + (this.xIsNext ? 'X' : 'O')
    },
  },
  methods: {
    handleClick(i) {
      const history = this.history.slice(0, this.stepNumber + 1)
      const current = history[history.length - 1]
      const squares = current.squares.slice()
      if (calculateWinner(squares) || squares[i]) {
        return
      }
      squares[i] = this.xIsNext ? 'X' : 'O'
      this.history = history.concat([{ squares: squares }])
      this.stepNumber = history.length
      this.xIsNext = !this.xIsNext
    },
    jumpTo(step) {
      this.stepNumber = step
      this.xIsNext = (step % 2) === 0
    },
  }
}
</script>
```

Gameコンポーネントはゲーム全体の状態を管理し，履歴を遡るための機能を提供している。
やはりレンダリングテンプレートを関数にできないことから，`<template>`の中がReactに比べて無理をしがち。履歴制御ボタンの`@click`やテキストにそのあたりが表れている。
Vue.jsの流儀であれば，ここもコンポーネントにするべきなのだろうが，今回はReactチュートリアルとの比較しやすさをとった。

# App.vue

```html:App.vue
<template>
  <div id="app">
    <game />
  </div>
</template>

<style lang="scss">
#app {
  font: 14px "Century Gothic", Futura, sans-serif;
  margin: 20px;
}
</style>

<script>
import Game from '@/components/Game'

export default {
  name: 'App',
  components: {
    Game,
  },
}
</script>
```

最後にGameコンポーネントを呼び出すようにApp.vueを修正。

動作画面はこんな感じ。
![tic-tac-toe.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/229548/78b04edc-064a-e000-8636-5bcac009a5b2.png)

# 今回の気付き

- ReactではレンダリングテンプレートがJavaScriptであることにより，関数化などの柔軟な実装が可能。
- 下位コンポーネントで発生したイベントを，上位コンポーネントのハンドラで処理するためにプロパティが使える（要調査）。
- v-forで生成される要素に固有のハンドラを割り当てるために，`@click`にアロー関数を設定する方法が使える。