Use Vue.js to Rewrite React's Official Tutorial Tic Tac Toe

React’s official 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

First you can see the final result, and try to click and experience, we will fulfill this effect gradually

Initial Code

Initial Effect

Open Initial status 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

1
2
3
4
5
6
7
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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

1
2
3
4
5
6
7
8
9
10
11
12
13
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

1
<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

1
2
3
4
5
6
7
8
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: number 0 - 8 are filled into the tic tac toe respectively

Add Interactive

Add click event to button element to update value

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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: 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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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

1
2
3
4
5
6
7
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: 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

1
2
3
4
5
6
7
8
9
10
11
12
13
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: click the tic tac toe grid, X and O will play alternatively

Determine Winner

Add function the calculate winner

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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

1
2
3
4
5
6
7
8
9
10
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: 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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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

1
2
3
4
5
6
7
Vue.component('Board', {
props: ['squares'],
methods: {
handleClick(i) {
this.$emit('click', i);
}
},

Current code and effect: 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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
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: 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