Guessing Game - with Vue
Let's now take a look at implementing the guessing game with Vue.js. We will still have a very similar backend, with Express.
You can follow along with the full code here
The main server file (guess.js
) configures Express with the necessary middleware:
const app = express();
app.use(express.urlencoded({ extended: true }))
app.use(bodyParser.json());
app.set('view engine', 'pug');
It also sets up session management to track game state across requests, just like in the past:
app.use(session({
secret: 'cmps369',
resave: false,
saveUninitialized: true,
cookie: { secure: false }
}))
The routing structure is modular, with dedicated route files for the game's main functionality:
app.use('/play', require('./routes/play'))
app.use('/history', require('./routes/history'))
app.use('/', (req, res) => {
res.render('guess', {});
});
The Layout Template
Our application begins with a base layout template (layout.pug
) that includes the necessary dependencies:
doctype html
html
head
title Vue Guess
meta(name="viewport", content="width=device-width,initial-scale=1")
link(rel="stylesheet", href="/guess.css")
script(src="https://unpkg.com/axios/dist/axios.min.js")
script(src="https://unpkg.com/vue@3/dist/vue.global.js")
body
block content
This template loads Vue.js and Axios from CDNs, providing the foundation for our client-side code. Notice how we're using Vue 3's global build, which allows us to use the Vue.createApp()
syntax.
The game
Now let's dive into the main game implementation in guess.pug
. This template extends the base layout and includes our Vue application code.
Each Vue application begins by defining its data model. For our guessing game, we need to track:
data: function () {
return {
guess: '', // The current user input
guesses: [], // History of past guesses and their results
success: false // Whether the player has won
}
}
This simple data structure drives the entire game. The beauty of Vue is that changes to these properties automatically update the UI without us having to write code to select elements and modify their content or attributes.
Vue components have lifecycle hooks that allow us to run code at specific times. Our game uses the mounted
hook to initialize a new game when the component is first rendered:
mounted: function() {
// This is called as soon as the Vue app is mounted to the DOM
// We issue a call to initialize the game.
this.init();
}
The init
method uses Axios to make an HTTP request to our server:
async init() {
const response = await axios.get('/play');
this.success = false;
this.guess = '';
// Array elements are reactive, but
// reseting the array is not reactive.
// So the preferred way to "clear" and array
// is to use splice
this.guesses.splice(0, this.guess.length);
}
This method does several important things:
- It requests a new game from the server
- It resets the game state (setting
success
to false and clearing the input) - It demonstrates a Vue reactivity best practice by using
splice
to clear the array while maintaining reactivity
Core Game Logic
The game's core logic is in the doGuess
method:
async doGuess() {
// Make a POST request to /play with the guess
const response = await axios.post('/play',
{
guess: this.guess
});
// Push the guess and result to the guesses array
this.guesses.push ({
guess: this.guess,
result: response.data.result
});
// If the result is correct, set success to true
if (response.data.result === 'complete') {
this.success = true;
}
this.guess = '';
}
This method:
- Sends the player's guess to the server
- Records the guess and the server's response in the
guesses
array - Updates the game state if the guess was correct
- Clears the input field for the next guess
The backend logic in routes/play.js
handles the actual comparison:
router.post('/', async (req, res) => {
const game = await req.db.findGame(req.session.gameId);
if (!game) {
res.status(400).send('Not Found');
return;
}
await req.db.recordGuess(game, req.body.guess);
const guess = parseInt(req.body.guess);
if (guess < game.secret) {
res.json({ result: "low" })
} else if (guess > game.secret) {
res.json({ result: "high" });
} else {
await req.db.complete(game);
res.json({ result: "complete" });
}
})
Declarative UI Rendering
The most powerful aspect of Vue is its declarative rendering approach. Let's look at how the game UI is defined:
.container#play
section(v-if="success")
h1 Great job!
a(href="#", @click='init()') Play again!
section(v-else)
p(v-if='guesses.length === 0') I'm thinking of a number from 1-10!
p(v-else) Sorry your guess was {{guesses[guesses.length - 1].result}}, try again!
p
label(for="guess") Enter your guess:
input(id="guess", v-model='guess', placeholder="1-10", type="number", min="1", max="10")
p
button(@click = 'doGuess()', type='type') Submit
section
ul
li(v-for='guess in guesses', :class="{correct: guess.result === 'complete', low: guess.result === 'low', high: guess.result === 'high'}")
span {{guess.guess}} is {{guess.result}}
This template uses several Vue directives to create a dynamic UI:
v-if="success"
conditionally shows the success message when the player winsv-else
shows the game form when the player hasn't won yetv-if='guesses.length === 0'
shows a welcome message for new gamesv-else
shows feedback on the last guess for ongoing gamesv-model='guess'
creates a two-way binding between the input field and theguess
data property@click='doGuess()'
binds the button click to thedoGuess
methodv-for='guess in guesses'
creates a list item for each past guess:class="..."
dynamically applies CSS classes based on the guess result
With vanilla JavaScript, implementing this UI would require:
- Creating event listeners for user inputs
- Writing DOM manipulation code to update elements
- Managing the synchronization between data and UI
- Carefully tracking the application state
With Vue, we simply describe what the UI should look like in each state, and Vue handles all the updates automatically.
Game History: Working with Lists
The game history feature demonstrates Vue's powerful list rendering capabilities. The history.pug
template creates a table of previous games:
.container#history
table
thead
tr
th Game ID
th Complete
th Num Guesses
th Started
tbody
tr(v-for='g in games')
td: a(:href="'/history/'+g.id") {{g.id}}
td: span(v-if='g.complete') Yes
td {{g.num_guesses}}
td {{g.time}}
This template uses:
v-for='g in games'
to create a table row for each game:href="'/history/'+g.id"
to create dynamic links to game detailsv-if='g.complete'
to conditionally show the "Yes" text for completed games
The Vue component fetches the game data when it's mounted:
Vue.createApp({
data: function () {
return {
games: []
}
},
mounted: async function() {
const response = await axios.get('/history/games');
this.games = response.data;
}
}).mount('#history')
Individual Game Details: Dynamic Content Loading
The game.pug
template shows the details of a specific game. It demonstrates how server-side data can be injected into Vue components:
mounted: async function() {
// This is tricky. The pug model has the game id, and we
// are putting it in the source code here. Do a view-source
// in your browser to see the game id.
const response = await axios.get('/history/#{game_id}/guesses');
this.game_guesses = response.data;
}
The #{game_id}
syntax is a Pug interpolation that inserts the game ID provided by the server. This allows the Vue component to fetch the specific guesses for this game.
Comparing to Vanilla JavaScript
If we were to implement this game with vanilla JavaScript, we would need to:
- Write code to select DOM elements
- Manually update element content when data changes
- Create event listeners for user interactions
- Maintain a mental model of the application state
- Write code to synchronize the data and the UI
For example, displaying the list of guesses might look like:
function updateGuessList() {
const guessList = document.querySelector('ul');
guessList.innerHTML = '';
for (const guess of guesses) {
const li = document.createElement('li');
li.textContent = `${guess.guess} is ${guess.result}`;
li.classList.add(guess.result);
guessList.appendChild(li);
}
}
With Vue, we simply declare:
ul
li(v-for='guess in guesses', :class="guess.result")
span {{guess.guess}} is {{guess.result}}
And Vue handles all the DOM manipulation for us, automatically updating the list when the guesses
array changes.
The API Integration Pattern
The application follows a clean pattern for API integration:
- Vue components make HTTP requests to the server using Axios
- The server processes the requests and returns JSON responses
- Vue updates its data model with the response data
- The UI automatically updates to reflect the new data
This pattern decouples the frontend and backend, making it easier to maintain and test each part independently.
Conclusion
Vue.js transforms how we build web applications by shifting from imperative to declarative programming. Rather than writing code that describes how to update the UI, we write code that describes what the UI should look like in each state.
This guessing game demonstrates Vue's key features:
- Reactive data binding
- Declarative rendering
- Component-based architecture
- Lifecycle hooks
- Event handling
- List rendering
By leveraging these features, we can build more maintainable, testable, and scalable web applications with less code and fewer bugs.