Use Vue.js to rewrite React's Official Tutorial Tic Tac Toe
Tags: vue, javascript
Published: 2020-05-05 01:36:55

React's official [tutorial][react-tutorial] tic tac toe do a good job to guide the newbie to enter React's world step by step, I think that similar tutorial will inspire Vue.js's newbie, so I use Vue.js to rewrite it
<!-- more -->
First you can see the final [result][codepen-done], and try to click and experience, we will fulfill this effect gradually


## Initial Code 

### Initial Effect

Open [Initial status][codepen-init] and edit directly, or copy the code to corresponding files in the same directory  
For now it's just a simple tic tac toe grid, and a hard-coded "Next Player"

### Initial Code Description

Now three component has been defined, which are `Square`, `Board` and `Game` respectively

Square is just a normal button now
```
Vue.component('Square', {
  template: `
    <button class="square">
      {{ /* TODO */ }}
    </button>
  `
})
```
- After component is defined like this, other component can use \<Square /> to reference this component directly

Board component is composed by current status and 9 Square
```
Vue.component('Board', {
  data() {
    return {
      status: `${nextLabel}X`,
      board: [
        [0, 1, 2],
        [3, 4, 5],
        [6, 7, 8]
      ]
    }
  },
  template: `
    <div>
      <div class="status">{{ status }}</div>
      <div class="board-row" v-for="(row, index) in board" :key="index">
        <Square v-for="square in row" :key="square" />
      </div>
    </div>
  `
});
```
- the current `status` and value of `board` is defined in `data`, thus you can use `{{ status }}` to reference the value of status, and use `v-for` to iterate two dimension array `board` twice to compose tic tac toe grid  
- `data` in component must be a function which return a object, but not literal object
- `v-for` must have `key` to make sure performance without alert

`Game` component is formed by `Board`, and status and history which will be added later
```
Vue.component('Game', {
  template: `
    <div class="game">
      <div class="game-board">
        <Board />
      </div>
      <div class="game-info">
        <div>{{ /* status */ }}</div>
        <ol>{{ /* TODO */ }}</ol>
      </div>
    </div>
  `
});
```


## Add Data Handling

### Add Props

Deliver a `prop` whose name is `value` to Square in Board
```
<Square v-for="square in row" :key="square" :value="square" />
```
- :value is short for v-bind:value, which means that its value is an expression

Add `value` prop in the component definition and template of Square
```
Vue.component('Square', {
  props: ['value'],
  template: `
    <button class="square">
      {{ value }}
    </button>
  `
})
```
- `props` are variables which parent component can deliver to child component, set corresponding attribute in tag when parent component invoke child component, and the usage method is the same as `data` in child component 

Current [code and effect][codepen-props]: number 0 - 8 are filled into the tic tac toe respectively 

### Add Interactive 

Add click event to button element to update value
```
Vue.component('Square', {
  //props: ['value'],
  data() {
    return {
      value: null
    }
  },
  methods: {
    setValue() {
      this.value = 'X';
    }
  },
  template: `
    <button class="square" @click="setValue">
      {{ value }}
    </button>
  `
})
```
- `@click` is short for `v-on:click`, whose value is the function which will run when click, here set to setValue which is defined in methods of the component
- Child component can not update data of parent directly, so change value from props to data
- `data`'s value will be updated, and corresponding template will update automatically to display the content.

Current [Code and Effect][codepen-click]: click the tic tac toe grip, the cell will be filled by X


## Improve Game

### Data Upward

To alternatively play and confirm winner, we need to determine status of every cell uniformly, so the value will be lifted to Board

Add data `squares` and method `handleClick` to Board
``` 
Vue.component('Board', {
  data() {
    return {
      ...
      squares: Array(9).fill(null),
    }
  },
  methods: {
    handleClick(i) {
      const squares = this.squares.slice();
      if (squares[i]){
        alert('Place was taken!');
        return
      }
      squares[i] = 'X';
      this.squares = squares;
    }
  },
  template: `
    ...
      <div class="board-row" v-for="(row, index) in board" :key="index">
        <Square v-for="square in row" :key="square" :value="squares[square]" @click="handleClick(square)" />
```
- Init `squares` to a array with 9 null, so the tic tac toe grip will be empty
- `handleClick` accepts parameter of corresponding cell number, and will update corresponding `square` element
- the event handler is not the return value of  `handleClick(square)`, but `handleClick`, and `square` will be parameter when trigger

Trigger click event of Board in the click event handler of Square
```
Vue.component('Square', {
  props: ['value'],
  methods: {
    setValue() {
      this.$emit('click');
    }
  },
```
- `value` need to be changed from data back to `props`
- `$emit` can invoke event handler which parent component deliver
- value of prop is updated in parent component, and the child template will update display content correspondingly

Current [Code and Effect][codepen-emit]: click the tic tac toe grid, if it's not taken, it will be filled with X

### Play Alternatively

Add data `xIsNext`, and switch when click
```
data() {
  return {
    ...
    xIsNext: true
  }
},
methods: {
    handleClick(i) {
      ...
      squares[i] = this.xIsNext ? 'X' : 'O';
      this.squares = squares;
      this.xIsNext = !this.xIsNext;
      this.status = `${nextLabel}${this.xIsNext ? 'X' : 'O'}`;
```
- Init `xIsNext` as `true`, which means X will be first player  
- After click, reverse xIsNext to switch
- Update `status` to the next player

current [code and effect][codepen-alter]: click the tic tac toe grid, X and O will play alternatively

### Determine Winner

Add function the calculate winner
```
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;
}
```
- Enumerate the combination which will win the game, and compare with value of `squares` array

Add winner logic of click handler function
```
if (calculateWinner(squares)) {
  alert('Winner was determined!');
  return;
}
...
const winner = calculateWinner(squares);
if (winner) {
  this.status = 'Winner: ' + winner;
  return;
}
```
- After click, if there's winner before, than the clicking is invalid
- After placement proceeding, judge winner again, and update status

Current [code and effect][codepen-winner]: status and click handler will be updated after one side win

## Add Time Tour

### Save History Record

To fulfill functionality of retraction, we need to record entire status of every placement, equivalent to the snapshot of the chessboard, which will became a history record, upward to the `Game` component

Add `history` data in `Game`, transfer `xIsNext`, `status` and `handleClick` method from Board to Game 
```
Vue.component('Game', {
  data() {
    return {
      history: [{
        squares: Array(9).fill(null),
      }],
      xIsNext: true,
      status: `${nextLabel}X`
    }
  },
  methods: {
    handleClick(i) {
      const history = this.history;
      const current = history[history.length - 1]
      const squares = current.squares.slice();
      ...
      squares[i] = this.xIsNext ? 'X' : 'O';
      history.push({
        squares: squares
      });
      ...
    }
  },
  template: `
    <div class="game">
      <div class="game-board">
        <Board :squares="history[history.length - 1].squares" @click="handleClick" />
  `
})
```
- Utilize the last record of history to assign value to squares (only one record for now)
- After placement, squares will record the placement, and history will add a record

Add prop squares to Board, and update handleClick to invoke event handler of parent component
```
Vue.component('Board', {
  props: ['squares'],
  methods: {
    handleClick(i) {
      this.$emit('click', i);
    }
  },
```

Current [code and effect][codepen-history]: status location is updated, and store history is recorded

### Show History Step Record

Iterate history records to display, and bind click event, show record of corresponding step via update of `stepNumber`
```
Vue.component('Game', {
  data() {
    ...
      stepNumber: 0,
    ...
    }
  },
  methods: {
    handleClick(i) {
      const history = this.history.slice(0, this.stepNumber + 1);
      ...
      this.history = history.concat([{
        squares: squares
      }]);
      this.stepNumber = history.length;
      ...
    },
    jumpTo(step) {
      if(step === this.stepNumber){
        alert('Already at ' + (0 === step ? 'Beginning' : `Step#${step}!`));
        return;
      }
      this.stepNumber = step;
      this.xIsNext = (step % 2) === 0;
      this.status = `${nextLabel}${this.xIsNext ? 'X' : 'O'}`;
    }
  },
  template: `
    <div class="game">
      <div class="game-board">
        <Board :squares="history[this.stepNumber].squares" @click="handleClick" />
      </div>
      <div class="game-info">
        <div>{{ status }}</div>
        <ol>
          <li v-for="(squares, index) in history" :key="index" :class="{'move-on': index === stepNumber}">
            <button @click="jumpTo(index)">{{ 0 === index ? 'Go to start' : 'Go to move#' + index }}</button>
   ...
  `
})
```
- Add `stepNumber` in Game, and init it as `0`, record current display step
- Utilize corresponding step of `this.stepNumber` to assign value to prop `squares` of Board 
- Handle `history` with current step as foundation in handleClick, and update stepNumber
- Add method `jumpTo` to handle display of going back to history, update `stepNumber`, `xIsNext` and `status`

Current [code and effect][codepen-done]: there will be one more history step after every placement, and click the step can return to this step


## Summarize

### Game Accomplishment
- Place cell alternatively
- Determine Winner
- Retract and play again

### Technology Showcase
- v-bind: bind data in template
- v-for: Iterate array in template
- v-on, $emit: transfer and trigger event between components
- data: define in component and automatically update in template
- prop: transfer between components and automatically update in template 


[react-tutorial]: https://reactjs.org/tutorial/tutorial.html
[codepen-done]: https://codepen.io/chanvin/pen/rNVZwJy?editors=0010
[codepen-init]: https://codepen.io/chanvin/pen/yLNxVdL
[codepen-props]: https://codepen.io/chanvin/pen/wvaEgPV?editors=0010
[codepen-click]: https://codepen.io/chanvin/pen/jOPvyxW?editors=0010
[codepen-emit]: https://codepen.io/chanvin/pen/mdJGWxV?editors=0010
[codepen-alter]: https://codepen.io/chanvin/pen/ZEGMKBZ?editors=0010
[codepen-winner]: https://codepen.io/chanvin/pen/PoqdmEQ?editors=0010
[codepen-history]: https://codepen.io/chanvin/pen/poJOwbv?editors=0010