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:

  1. It requests a new game from the server
  2. It resets the game state (setting success to false and clearing the input)
  3. 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:

  1. Sends the player's guess to the server
  2. Records the guess and the server's response in the guesses array
  3. Updates the game state if the guess was correct
  4. 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 wins
  • v-else shows the game form when the player hasn't won yet
  • v-if='guesses.length === 0' shows a welcome message for new games
  • v-else shows feedback on the last guess for ongoing games
  • v-model='guess' creates a two-way binding between the input field and the guess data property
  • @click='doGuess()' binds the button click to the doGuess method
  • v-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:

  1. Creating event listeners for user inputs
  2. Writing DOM manipulation code to update elements
  3. Managing the synchronization between data and UI
  4. 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 details
  • v-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:

  1. Write code to select DOM elements
  2. Manually update element content when data changes
  3. Create event listeners for user interactions
  4. Maintain a mental model of the application state
  5. 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:

  1. Vue components make HTTP requests to the server using Axios
  2. The server processes the requests and returns JSON responses
  3. Vue updates its data model with the response data
  4. 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.