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
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 ofboard
is defined indata
, thus you can use{{ status }}
to reference the value of status, and usev-for
to iterate two dimension arrayboard
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 havekey
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 asdata
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
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 forv-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
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 correspondingsquare
element - the event handler is not the return value of
handleClick(square)
, buthandleClick
, andsquare
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 toprops
-
$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
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
astrue
, 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
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: 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: 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 as0
, record current display step - Utilize corresponding step of
this.stepNumber
to assign value to propsquares
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, updatestepNumber
,xIsNext
andstatus
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
Top comments (0)