DEV Community

Cover image for Hangman with Vue 3.0
Abdul Basit
Abdul Basit

Posted on

Hangman with Vue 3.0

1. Table of Contents

2. Introduction

I had taken Brad Traversy's hangman game, which he made in his 20 Web Projects With Vanilla JavaScript Udemy course, and remade it using Vue 3, I also added a few things of my own and changed some styles. In this article I am not going to focus on explaining the styles, just copy & paste them form this post.
You can find the code for the actual game in his GitHub repo for the course. You can find the the code for this project in this repo. You can also find a live demo of the game here

In this article I am going to tell you how I did it. In this way you can learn about the new features of Vue 3. You can learn about the differences between the v2 & v3 on the official v3 migration guide. However the new features that I have used are as follows:

  • Typescript - Vue3 have full support for typescript, since it is completely rewritten in typescript.
  • Composition API - A new API, in addition to the old Options API which still has full support, that makes things a lot easier.
  • Reactivity API - A new addition in Vue3 it exposes functions to make reactive variables & objects, create computed properties, watcher functions and much more. This API is a must when using Composition API.
  • Fragments - Vue now supports fragments, if you don't know what fragments are then we are going discuss about fragments later.

So let's get started!

3. Requirements

Requirements for this project are:

3.1. Nodejs & NPM

Nodejs is required to run the Vue CLI and compiler. We also need a package manger, I use npm but you use yarn if you want to.

If you don't have it then download the installer for the latest LTS version from their website and install it, make sure you also install NPM.

3.2. Vuejs 3.0

Obviously, that's the title.
If you already have installed the newest version of the vue cli than good, else just run the following command to install it.

npm i -g @vue/cli
Enter fullscreen mode Exit fullscreen mode

3.3. A code Editor

Personally I prefer VSCode (and so do most of the developers).
If you are using VSCode then sure you install the Vetur extension. You can use any other code editor if you want to.

4. Creating the project

Open up your commandline and change your directory to where you want to make this project. Initialize a new vue project by running the following command:

vue create hangman
Enter fullscreen mode Exit fullscreen mode

It will ask you about a preset:

Preset

Select manually and hit enter.

Next it will ask about what features do we want:

Features

For our project we will be using typescript, scss, vue-router, and eslint. So select the following and hit enter.

Selected Features

Next it will ask which version of vue do we want to use:

Version

Select 3.x(Preview) and hit enter.

Next, it will ask us a couple of yes/no questions. Answer as following:

Y/N Questions

Next, it will ask us which CSS Pre-processor do want to use. Select Sass/SCSS (with node-sass) and hit enter.

Then, it will ask us to pick a linter config. Select ESLint + Standard config and hit enter. It will also ask us about some additional linting features:

Lint Features

Select both and hit enter.

Then it will ask us where we want to put our config for different stuff. Select what you want and hit enter. It will also ask us if we want to save these settings as a preset for future project answer what you want and hit enter.

Once the setup is complete then in your cd into hangman. If you are using VSCode then type

code .
Enter fullscreen mode Exit fullscreen mode

and hit enter this will open code with the project folder. Now you can close your command prompt. Form now on we will be using VSCode's integrated terminal.

5. Initial Setup

Open VSCode's integrated terminal and run the following command

npm run serve
Enter fullscreen mode Exit fullscreen mode

This will start the vue compiler with development mode & start a dev server at localhost:8080 and open it in the browser and it will look like this:
Initial App

We also need to install an npm package random-words, as the name suggests we are going to use it to get a random word every time. So run the following in the project root folder in any shell:

npm i random-words
Enter fullscreen mode Exit fullscreen mode

Open the main.ts file in the src folder, it will look like this :

import { createApp } from 'vue';
import App from './App.vue';
import router from './router';

createApp(App)
    .use(router)
    .mount('#app');
Enter fullscreen mode Exit fullscreen mode

Here we can see the new approach to create new objects, e.g. new app, new router, new store, e.t.c. in Vue3, Vue2 provided us with classes that we could use to create new object, but Vue3 provides us with functions to create objects that are the basis of our app. As you can see here we are importing the new createApp function from vue with which we are creating a new app. Since this functions returns us our app therefore we have to use this app to define global stuff, e.g. plugins, components, e.t.c. And we can longer do this in our configuration files of the plugins.

Now in our project directory in the src folder open up the App.vue file.

<template>
    <div id="nav">
        <router-link to="/">Home</router-link> |
        <router-link to="/about">About</router-link>
    </div>
    <router-view />
</template>

<style lang="scss">
#app {
    font-family: Avenir, Helvetica, Arial, sans-serif;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
    text-align: center;
    color: #2c3e50;
}

#nav {
    padding: 30px;

    a {
        font-weight: bold;
        color: #2c3e50;

        &.router-link-exact-active {
            color: #42b983;
        }
    }
}
</style>
Enter fullscreen mode Exit fullscreen mode

Remove every thing except the <router-view /> from the template and copy and paste the following styles instead in the style:

* {
        box-sizing: border-box;
}

body {
    margin: 0;
    padding: 50px 0 0 0;
    background-color: #2b2b6d;
    color: #ffffff;
    font-family: Tahoma;

    display: grid;
    place-content: center;
    place-items: center;
    text-align: center;
}

h1,
h2,
h3,
h4 {
    font-weight: 500;
}

main {
    position: relative;
    width: 800px;
}
Enter fullscreen mode Exit fullscreen mode

Now in the views directory open Home.vue. It will look like this

<template>
  <div class="home">
    <img alt="Vue logo" src="../assets/logo.png" />
    <HelloWorld msg="Welcome to Your Vue.js + TypeScript App" />
  </div>
</template>

<script lang="ts">
import { defineComponent } from "vue";
import HelloWorld from "@/components/HelloWorld.vue"; // @ is an alias to /src

export default defineComponent({
  name: "Home",
  components: {
    HelloWorld
  }
});
</script>
Enter fullscreen mode Exit fullscreen mode

Remove every thing form the <div> in template also remove the import of the HelloWorld.vue and also remove it from the components option.

In the components directory in the src folder delete the HelloWorld.vue file.

Now in the browser it will just be a purple color.

6. Starting Up

We are going to build our game in the Home.vue file so open it up. In here we are going to use a new feature in Vue3 fragments. In the template we are now going to replace that div with a simple <header> and a <main> tag. It will now look like this:

<header>
    <h1>Hangman</h1>
    <p>Find the hidden word enter a letter</p>
</header>

<main></main>
Enter fullscreen mode Exit fullscreen mode

Fragments allow us to have multiple root nodes. So now we don't require to wrap all of this up in a div to have only one root node. In the browser it will now show us a header.

So now let's break up our game and see how it is going to work. In an hangman game a random word is chosen and we are told the no. of letters in that word. We have to guess the word by guessing one letter at a time if that letter is present in the word then it is written at position(s) it is present at in the word, if the letter is not present then it joins the list of wrong letters and the next body part of a stick-man is drawn. The total number of body parts of stick-man is 6(1 head, 1 stick for neck & belly, 2 arms and 2 legs). If we guess the word before the the drawing of the stick-man is complete then we win, else if the we can't guess the word and the drawing of the stick-man is complete then the stick-man is hanged and we lose. If a letter that have already been called is called again whether it was correct or wrong it doesn't count and we are notified that the letter have already been called.

If we understand all of the above then we will see that there is actually a lot stuff that needs to be done. We require the following:

  1. A random word (we are going to use the random-words package for this)
  2. Show visually how many letters are in the word.
  3. Functionality to enter letters one by one.
  4. If the letter is correct then show it in its place.
  5. Else if the letter is wrong then it joins the list of wrong letters, which we need to show on the screen.
  6. And the next body part of the stick-man is drawn.
  7. If a letter is entered again then we need to show a notification saying letter has already been entered.
  8. If the user correctly guesses the word before the man is hanged then we need to stop the play and show a popup saying they won.
  9. If the drawing of the stick-man is complete then we need to stop the play and show them a popup saying that they lost and also tell them the correct word.
  10. On the popup we also need to have a play again button.
  11. For additional features and to learn some more things we are also going to build a page that tells the users the words that they have correctly guessed.
  12. For this purpose we also need a global state.

If we see above requirements then we need two views. We are also going to build different components for the word, wrong letters, stick-man, notification and popup to simplify our work.

7. Making the Game

In the script tag of the Home.vue file you will notice that the component has been defined with a defineComponent() method. This method is used only when using type script to get proper type inference.

In the defineComponent add a the new setup() method, this method is the whole new Composition API this allows us to group our functionality together which will be far apart in the old Options API. This method is called when creating the component and it returns state and methods for our component. It takes up to two arguments, but we will talk about them later.

In the setup() method the variables we declare are not reactive, if we want to make any variable reactive then we can do so with the new ref() method, we just have to import the for vue.

import { defineComponent, ref } from 'vue';
Enter fullscreen mode Exit fullscreen mode

Then in the setup method we need quite a few reactive variables:

const word = ref('');

const correctLetters = ref<Array<string>>([]);

const wrongLetters = ref<Array<string>>([]);

const notification = ref(false);
const popup = ref(false);
const status = ref('');
Enter fullscreen mode Exit fullscreen mode

The ref() method takes the initial value of a variable as the argument and returns it wrapped within an object with a value property, which can then be used to access or mutate the value of the reactive variable. This is used to create pass by reference functionality because in JS the primitive types are passed by value and not by reference. This allows us to pass its value across our app without losing its reactivity.

We have defined 6 variables so far. Let's see what they are for:

  • Word is the word which needs to be guessed,
  • correctLetters it is the array of letters correctly guessed,
  • wrongLetters it is the array of letters entered that were wrong,
  • notification and popup are boolean for either of them to be visible,
  • status is the status of the game . It is an empty string if the game is in play or else won or lost

We also define a boolean variable for starting and stopping the game:

let playable = true;
Enter fullscreen mode Exit fullscreen mode

Going ahead , now we are going to import the random-words at the top:

import randomWord from 'random-words';
Enter fullscreen mode Exit fullscreen mode

The randomWord method is going to give us a random word every time we call it.

Next we are going to define play method:

const play = () => {
    word.value = randomWord();
    correctLetters.value = [];
    wrongLetters.value = [];
    status.value = '';
    playable = true;
    popup.value = false;
};
Enter fullscreen mode Exit fullscreen mode

Here, we are setting the values of each variable to it's initial value. Except word which we are setting to a random word. Next we are going to define gameOver method:

const gameOver = (result: string) => {
    playable = false;

    status.value = result;

    popup.value = true;
};
Enter fullscreen mode Exit fullscreen mode

This method takes in the result of the game sets the playable to false, the value of popup to true to show the popup and sets the value of status to result

Next up, we are going to create the showNotification method which sets the value of notification to true and sets it to false again after 1s(1000ms)

const showNotification = () => {
    notification.value = true;
    setTimeout(() => (notification.value = false), 1000);
};
Enter fullscreen mode Exit fullscreen mode

After that, we are going to create a method for event listener for keydown event that we are going to add in a lifecycle method of the component. This method obviously takes in a KeyboardEvent as an argument. Then we destructure it to take the key & keyCode out of it. Then we check if the game is playable and if the keyCode is between 60 & 90, it means if is key entered is an lowercase or uppercase letter. If all those conditions are met then we transform the key to lowercase then check if the current word includes the key. If it does then we check if the array of correctLetters doesn't include the key, if doesn't then we set the value of correctLetters to an array whose first element is key and copy the correctLetters to this new array with spread operator (this creates an array that all the elements of correctLetters plus the key) else we call the showNotification() method. If the word doesn't includes the key then we have same procedure for wrongLetters as we did for correctLetters.

const keyDown = (e: KeyboardEvent) => {
    let { keyCode, key } = e;

    if (playable && keyCode >= 60 && keyCode <= 90) {
        key = key.toLowerCase();

        if (word.value.includes(key))
            !correctLetters.value.includes(key)
                ? (correctLetters.value = [key, ...correctLetters.value])
                : showNotification();
        else
            !wrongLetters.value.includes(key)
                ? (wrongLetters.value = [key, ...wrongLetters.value])
                : showNotification();
    }
};
Enter fullscreen mode Exit fullscreen mode

The one thing to know about the setup() method is that as is name suggests it is the setup of the component means the component is created after it runs, therefore with the exception of props we have no access to any properties declared in the component neither we can create any lifecycle methods but we can register lifecycle hooks inside setup() by importing several new functions from vue. They have the same name as for Options API but are prefixed with on: i.e. mounted will be onMounted. These functions accept a callback which will be called by the component. Further more 2 lifecycle methods have been renamed:

  • destroyed is now unmounted &
  • beforeDestroy is now beforeUnmount.

We are going to register three lifecycle hooks:

  • onBeforeMount: Here we are going to add the eventListener, for keyup, on the window.
  • onMounted: Here we are going to call the play method.
  • onUnounted: Here we are going to remove the event listener.

We are going to import the functions form vue:

import { defineComponent, ref, onBeforeMount, onMounted, onUnmounted } from 'vue';
Enter fullscreen mode Exit fullscreen mode

Next we are going to call these functions to register the hooks:

onBeforeMount(() => window.addEventListener('keydown', keyDown));

onMounted(() => play());

onUnmounted(() => window.removeEventListener('keydown', keyDown));
Enter fullscreen mode Exit fullscreen mode

At the end we need to return an object with all the variables and methods that we are going to use in the component:

return {
    word,
    correctLetters,
    wrongLetters,
    notification,
    popup,
    status,
    play,
    gameOver,
};
Enter fullscreen mode Exit fullscreen mode

This is all for functionality of our main view. Though we are not done with it when create all the components next we are going to import them in here and use them.

7.1. Snippet

Following is the snippet that we are going to use to scaffold our every component:

<template>
    <div></div>
</template>

<script lang="ts" >
    import { defineComponent } from "vue";

    export default defineComponent({
        name: '',
    });
</script>

<style lang="scss" scoped>
</style>
Enter fullscreen mode Exit fullscreen mode

7.2. GameFigure Component

The first component that we are going to create is the figure of the stick-man plus the hanging pole. In the components folder in the src directory create a new file and name it GameFigure.vue. Scaffold it with the above given snippet.

The template for this component is just svg:

<svg height="250" width="200">
    <!-- Rod -->
    <line x1="60" y1="20" x2="140" y2="20" />
    <line x1="140" y1="20" x2="140" y2="50" />
    <line x1="60" y1="20" x2="60" y2="230" />
    <line x1="20" y1="230" x2="100" y2="230" />

    <!-- Head -->
    <circle cx="140" cy="70" r="20" />
    <!-- Body -->
    <line x1="140" y1="90" x2="140" y2="150" />
    <!-- Arms -->
    <line x1="140" y1="120" x2="120" y2="100" />
    <line v-if="errors > 3" x1="140" y1="120" x2="160" y2="100" />
    <!-- Legs -->
    <line x1="140" y1="150" x2="120" y2="180" />
    <line x1="140" y1="150" x2="160" y2="180" />
</svg>
Enter fullscreen mode Exit fullscreen mode

Before working on the functionality, we are going to add the styles. Copy and paste the following in the <style> tag:

svg {
    fill: none;
    stroke: #fff;
    stroke-width: 3px;
    stroke-linecap: round;
}
Enter fullscreen mode Exit fullscreen mode

The functionality for this component is very simple. It is going to get errors, the no. of errors made, as a prop and is going to watch errors as soon as errors' value is six it is going to emit a gameover event. So we are going to use the Options API and not the Composition API:

export default defineComponent({
    name: 'GameFigure',
    props: {
        errors: {
            type: Number,
            default: 0,
            required: true,
            validator: (v: number) => v >= 0 && v <= 6,
        },
    },
    emits: ['gameover'],
    watch: {
        errors(v: number) {
            if (v === 6) this.$emit('gameover');
        },
    },
});
Enter fullscreen mode Exit fullscreen mode

A new addition in Vue3 is the emits option, it used to document the events emitted by the component. It can be an array of events' or an object with events' names as properties whose values maybe validators for events. Here we are just using an array to tell the component emits gameover event.

We are going to conditionally render the body parts of the figure based on the no. of errors with v-if:

<!-- Head -->
<circle v-if="errors > 0" cx="140" cy="70" r="20" />
<!-- Body -->
<line v-if="errors > 1" x1="140" y1="90" x2="140" y2="150" />
<!-- Arms -->
<line v-if="errors > 2" x1="140" y1="120" x2="120" y2="100" />
<line v-if="errors > 3" x1="140" y1="120" x2="160" y2="100" />
<!-- Legs -->
<line v-if="errors > 4" x1="140" y1="150" x2="120" y2="180" />
<line v-if="errors > 5" x1="140" y1="150" x2="160" y2="180" />
Enter fullscreen mode Exit fullscreen mode

This is all we need to do for this component. Now we are going to use it in Home.vue.

Open Home.vue, import the component in the script tag and add it in the components object:

    import GameFigure from '@/components/GameFigure.vue';
    ...

    Component: {
        GameFigure,
    },
Enter fullscreen mode Exit fullscreen mode

Now in the main tag we are going to use this component, we are going to bind the errors with v-bind to the length of wrongLetters:

<main>
    <game-figure :errors="wrongLetters.length" />
</main>
Enter fullscreen mode Exit fullscreen mode

Now, if we look in the browser we will just see the hanging pole:
GameFigure Component Done

7.3. GameWord Component

Next up we are going to GameWord component. First of create a new file in the components directory and name it GameWord.vue and scaffold it with the above given snippet. It has quite a bit of functionality so we are going to use the Composition API.

First of all copy and paste the following in the style tag:

span {
    border-bottom: 3px solid #2980b9;
    display: inline-flex;
    font-size: 30px;
    align-items: center;
    justify-content: center;
    margin: 0 3px;
    height: 50px;
    width: 20px;
}
Enter fullscreen mode Exit fullscreen mode

Now, for the functionality. We are going to show a dash for every unguessed letter of the word and for any guessed letters we want to show the letter above the dash. To achieve this we are going to take in the word and correctLetters as props.
Here we can set the the type of word to String but for the correctLetters we can only set the type to the Array and not Array<string>. The type of a prop accepts a Constructor method, existing or self made, of a class, the reason is type of a prop is a property and properties accepts values and not types. To provide more correct types for props we need to type cast the Constructor methods to the new propType interface provided by Vue3. The propType is a generic type which takes as an argument the type of the prop. First import it from vue and then define props:

import { defineComponent, PropType } from 'vue';
...

props: {
    word: {
        type: String,
        required: true,
    },
    correctLetters: {
        type: Array as PropType<Array<string>>,
        required: true,
    },
},
Enter fullscreen mode Exit fullscreen mode

As I mentioned earlier that the setup() method takes up to 2 arguments, which are:

  • props: passed to the component
  • context: it is a plain js object which exposes three component properties: emit, slots & attrs.

However, props is a reactive object therefore it can't be destructured, if we do so the the destructured variables won't be reactive. If we need to destructure then we can to do so by turning the properties of the props to reactive properties by the toRefs function imported from vue.
The context is just a plain js object, therefore it can be destructured.

First import the toRefs form vue:

import { defineComponent, toRefs } from 'vue';
Enter fullscreen mode Exit fullscreen mode

Then create the setup method after props, in our case we only need the emit method to emit the gameover event if all the letters are guessed. Also destructure the props with toRefs:

setup(props, { emit }) {
    const { word, correctLetters } = toRefs(props);
},
Enter fullscreen mode Exit fullscreen mode

Next we need to create a computed property which turns the word into an array of letters. Computed properties inside the setup component are created with the computed function, imported from vue, which takes in a callback function which returns the property. The computed then return the property wrapped inside an CompuedRef object, which works very similar to the Ref object except that it creates a connection between the property it is computed from to keep updating the its value.

import { defineComponent, toRefs, computed } from 'vue';
...

const letters = computed(() => {
    const array: Array<string> = [];

    word.value.split('').map(letter => array.push(letter));

    return array;
});
Enter fullscreen mode Exit fullscreen mode

Next up we need to watch the correctLetters. We can watch reactive variables using the watch function, imported from vue. The function takes in two arguments:

  • the variable to watch &
  • a callback function which is called every time the variables value is changed:
import { defineComponent, PropType , toRefs, computed, watch } from 'vue';
...

watch(correctLetters, () => {
    let flag = true;

    letters.value.forEach(letter => {
        if (!correctLetters.value.includes(letter)) flag = false;
    });

    if (flag) {
        emit('gameover');
    }
});
Enter fullscreen mode Exit fullscreen mode

At the end we need to return the computed property letters:

return {
    letters,
};
Enter fullscreen mode Exit fullscreen mode

Now in the template replace the <div> with <section> and inside of section we are going to put a the following:

<section>
    <span v-for="(letter, i) in letters" :key="i">{{ correctLetters.includes(letter) ? letter : '' }}</span>
</section>
Enter fullscreen mode Exit fullscreen mode

Here we are using a <section> and inside of the <section> we have a <span> and we are using the v-for directive to render a span for each object in the letters array we are binding the i (index of the letter) to the key. We are saying that if the correctLetters array includes the current letter then write the letter else its an empty string. Now whenever the user guesses a correctLetter it will be pushed to the array of correctLetters and prop binding will cause the loop to render again and the letter will be shown.

This all we need to do for this component. Now, lets import it in the Home.vue and add it to the components option:

import GameWord from '@/components/GameWord.vue';
...

components: {
    GameFigure,
    GameWord
},
Enter fullscreen mode Exit fullscreen mode

And now, lets use it in our template , after the game-figure component. We are going to bind the word & correctLetters prop to word & correctLetters. We are also listening for the gameover event and are calling the gameOver and passing 'won' to the result argument:

<game-word :word="word" :correctLetters="correctLetters" @gameover="gameOver('won')" />
Enter fullscreen mode Exit fullscreen mode

Now, in the browser it will show us the dashes for every letter:

GameWord component done first image

If we enter a correct letter it will show us and if we enter a wrong letter it will draw the next body part of the stick-man:

GameWord component done second image

But if we make six errors or guess the word it won't let us enter any new letter but won't do any thing else:

GameWord component done third image

7.4. WrongLetters Component

Now, we are going to create the WrongLetters component, which will show all the wrong letters entered. In the components directory create a new file and name it WrongLetters.vue, scaffold it with the above given snippet. This a fairly simple component. For the script part we only have a prop. Also for the prop import propType form vue:

import { defineComponent, PropType } from 'vue';
... 
props: {
    wrongLetters: {
        type: Array as PropType<Array<string>>,
        required: true,
    },
},
Enter fullscreen mode Exit fullscreen mode

In the template we have an <aside> tag inside of which we and <h3> and a <div> with a <span> on we have applied v-for directive that iterates over the wrongLetters array and show all the wrong letter. Here we have also the letter as the key because a letter will only occur once.

<aside>
    <h3>Wrong Letters</h3>
    <div>
        <span v-for="letter in wrongLetters" :key="letter">{{ letter }},</span>
    </div>
</aside>
Enter fullscreen mode Exit fullscreen mode

And lastly for the styles just copy & paste the following:

aside {
    position: absolute;
    top: 20px;
    left: 70%;
    display: flex;
    flex-direction: column;
    text-align: right;

    span {
        font-size: 24px;
    }
}
Enter fullscreen mode Exit fullscreen mode

Now, lets use it in the component. Import it in the Home.vue and add it in the components:

import WrongLetters from '@/components/WrongLetters.vue';
...
components: {
    GameFigure,
    GameWord,
    WrongLetters,
},
Enter fullscreen mode Exit fullscreen mode

In the template add it between the <game-figure /> and the <game-word /> components and bind the wrongLetters prop to the wrongLetters

<wrong-letters :wrongLetters="wrongLetters" />
Enter fullscreen mode Exit fullscreen mode

This is it for this component.

Wrong Letters component done

7.5. LetterNotification Component

Now, we are going to work on the notification that tells that the letter entered has already been entered. In the components directory make a new file and name it LetterNotification.vue. Scaffold it with the above given snippet. For the script tag we only have a prop show which, obviously, we are going to show and hide the component.

props: {
    show: {
        type: Boolean,
        required: true,
    },
},
Enter fullscreen mode Exit fullscreen mode

Before we work on the markup, copy & paste the following in the <style>:

div {
    position: absolute;
    opacity: 0;
    top: -10%;
    left: 40%;
    background-color: #333;
    width: 300px;
    border-radius: 30px;

    transition: 0.2s all ease-in-out;

    &.show {
        opacity: 1;
        top: 1%;
    }
}
Enter fullscreen mode Exit fullscreen mode

In the <template> we have a <div> with a <p> telling the user that they have already entered the letter. We also have a class binding on the div that adds or removes the class based on the truthiness of the show:

<div id="notification" :class="{ show: show }">
    <p>You have already entered this letter</p>
</div>
Enter fullscreen mode Exit fullscreen mode

Now, import it in the Home.vue and add it in the components option:

import LetterNotification from '@/components/LetterNotification.vue';
...

components: {
    GameFigure,
    GameWord,
    WrongLetters,
    LetterNotification
},
Enter fullscreen mode Exit fullscreen mode

Now, in the template after the <main> tag add the component and bind the show prop to the notification variable:

<letter-notification :show="notification" />
Enter fullscreen mode Exit fullscreen mode

Now, in the browser if we enter a letter again it will show us notification and a second later it is going to disappear:

LetterNotification Component done

7.6. GameOverPopup Component

Add a new file in the components directory and name it GameOverPopup.vue. Scaffold it with the above given snippet;

The script tag for this component is simple. It emits a playagin event and have a playAgain method to emit the event. Therefore we are going to use the Options API to define the method:

emits: ['playagain'],
methods: {
    playAgain() {
        this.$emit('playagain');
    },
},
Enter fullscreen mode Exit fullscreen mode

Again, before markup add the following styles to the <style>:

div {
    position: absolute;
    top: 25%;
    left: 35%;
    background-color: #191919;
    width: 400px;
    height: 300px;
    border-radius: 20px;
    display: grid;
    place-items: center;
    place-content: center;

    h3 {
        font-size: 30px;
        transform: translateY(-20px);
    }

    h4 {
        font-size: 25px;
        transform: translateY(-30px);

        span {
            font-weight: 600;
            color: #00ff7f;
        }
    }

    button {
        font-family: inherit;
        font-size: 20px;
        width: 120px;
        height: 35px;
        color: #00ff7f;
        background-color: transparent;
        border: 2px solid #00ff7f;
        border-radius: 20px;
        cursor: pointer;
        font-weight: 450;

        &:hover,
        &:focus {
            color: #191919;
            background-color: #00ff7f;
        }

        &:focus {
            outline: none;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

The template for the component is a bit different, it is a <div> with a <slot></slot> and a <button> with an event listener for click event, on which we are calling the playAgain method:

<div id="popup">
    <slot></slot>
    <button @click="playAgain">Play Again</button>
</div>
Enter fullscreen mode Exit fullscreen mode

I have used a different approach here using template slots. If you don't know what slots are then briefly slots are used to render markup inside of a child component that was written in parent component. You can learn more about slots here. I have used slot here because, now, we don't have to pass in any props , including show, status and word.

Now, in the Home.vue import the component and add it to the components option:

import GameOverPopup from '@/components/GameOverPopup.vue';
...

components: {
    GameFigure,
    GameWord,
    WrongLetters,
    LetterNotification,
    GameOverPopup,
},
Enter fullscreen mode Exit fullscreen mode

In the template after the letter-notification component add the component:

<game-over-popup @playagain="play" v-show="popup">
    <h3>You {{ status }} {{ status === 'won' ? '🎉' : '😢' }}</h3>
    <h4 v-if="status == 'lost'">
        The word is: <span>{{ word }}</span>
    </h4>
</game-over-popup>
Enter fullscreen mode Exit fullscreen mode

Here we are listening for the playagain event and calling the play on it. We are using the v-if directive here to conditionally render it based on the truthiness of the popup variable. In the component we have a <h3> which shows the status and a emoji based on the value of the status . Then we have an <h4> which is only rendered if the status is lost that show what the correct word is.

When the user wins or loses all of this will first be rendered in the Home component and then it will passed on to the slot of the GameOverPopup component. Then we will see the popup.

GameOverPopup Component Done (Won)

GameOverPopup Component Done (Lost)

And if we click the play again button the game will restart:

GameOverPopup Component Done (Play Again)

Our game is now complete.

8. Additional Stuff

To learn a little more about Vue3 I decided to make a page that shows all the words that the user have guessed correctly. This allows us work with vue-router v4-beta and see how to get type annotations work for vue router, that's why we installed it in the beginning. To make this work we also need state management, but since our global state is so simple we don't need vuex we can just make our own globally managed state.

8.1. Making Globally Managed State

In the src folder create a new folder and name it store. Inside the folder create a new file and name it index.ts. Inside the file first thing import the reactive function from vue:

import { reactive } from "vue";
Enter fullscreen mode Exit fullscreen mode

This function works exactly the same as the ref method just the difference is that the ref function is used for creating single values reactive while the reactive function is used for objects.

After the import create a constant object store which have a state property which is a reactive object with a property guessedWords which is an array of string. The store also have a method addWord which takes in a word and pushes it to the guessedWords array.

const store = {
    state: reactive({
        guessedWords: new Array<string>(),
    }),

    addWord(word: string) {
        this.state.guessedWords.push(word);
    },
};
Enter fullscreen mode Exit fullscreen mode

At the end export the store as the default export for the file:

export default store;
Enter fullscreen mode Exit fullscreen mode

This all we need to do to create a simple globally managed state.

8.2. Using The addWord method

Now, we are going to use the addWord method. Open the GameWord.vue component import the store:

import store from '@/store';
Enter fullscreen mode Exit fullscreen mode

Then in callback function for the watch of the correctLetters when we check for the flag and are emitting the gameover event, before emitting call the addWord method form store and pass in the value of the word:

if (flag) {
    store.addWord(word.value);

    emit('gameover');
}
Enter fullscreen mode Exit fullscreen mode

8.3. Creating the GuessedWords View

In the views folder delete the About.vue file, and for now don't pay any attention to the errors in the router folder. Create a new file in the same folder and name it, you guessed it correctly, GuessedWords.vue. Scaffold it with the above given snippet. In the script tag import store and in the data function return the state from the store:

import store from '@/store';
...

data() {
    return store.state
},
Enter fullscreen mode Exit fullscreen mode

Now in the <template> we have a <header> inside of which we have an <h1> that says Hangman and a <p> that says 'Words correctly guessed'. After that we have a <main> with a <ul> inside of which we have an <li> on which we have applied the v-for directive that iterates over the guessedWords array and renders every word:

<header>
    <h1>Hangman</h1>
    <p>Words correctly guessed</p>
</header>

<main>
    <ul>
        <li v-for="(word, i) in guessedWords" :key="i">{{ word }}</li>
    </ul>
</main>
Enter fullscreen mode Exit fullscreen mode

Copy & paste the following styles in the <style> tag:

li {
    list-style-type: none;
    font-weight: 600;
}
Enter fullscreen mode Exit fullscreen mode

8.4. Configuring Router

Now, we are going to configure the vue-router open the index.ts file in the router folder. It will look like this:

import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router';
import Home from '../views/Home.vue';

const routes: Array<RouteRecordRaw> = [
    {
        path: '/',
        name: 'Home',
        component: Home,
    },
    {
        path: '/about',
        name: 'About',
        // route level code-splitting
        // this generates a separate chunk (about.[hash].js) for this route
        // which is lazy-loaded when the route is visited.
        component: () => import(/* webpackChunkName: "about" */ '../views/About.vue'),
    },
];

const router = createRouter({
    history: createWebHistory(process.env.BASE_URL),
    routes,
});

export default router;
Enter fullscreen mode Exit fullscreen mode

Here you can see the difference between the v3 and the v4 of the vue-router. The common difference between the Vue2 and Vue3 is that Vue2 provided classes to create to objects for every thing app, router, vuex, e.t.c. but Vue3 provides functions to create every thing. This difference is also evident here. A router is now created with the createRouter, similar to creating an app, that takes in an object, with configuration for the router, as an argument. There are some differences in router's configuration, the most prominent is that the mode option has now been removed instead now we have three different options for the three different modes. Here we are using the history mode, so we have the history option which takes in the webHistory which we can create with the createWebHistory method imported from vue-router. For the typing of the routes the vue-router provides the type RouterRecordRaw. Since we have an array of routes we have an Array<RouterRecordRaw>. Every thing else about the vue-router is same. You can find more information about the vue-router here.

Previously we deleted the About.vue and that's the error the compiler is giving us that it can not find the module About.vue. Replace about with guessedWords and About with GuessedWords, also remove the comments:

{
    path: '/guessedWords',
    name: 'guessedWords',
    component: () => import('../views/GuessedWords.vue'),
},
Enter fullscreen mode Exit fullscreen mode

8.5. Adding Navigation

Now, we are going to add the navigation. Open the App.vue and before the <router-view /> add the following:

<nav>
    <router-link to="/">Home</router-link> |
    <router-link to="/guessedWords">Guessed Words</router-link>
</nav>
Enter fullscreen mode Exit fullscreen mode

And the following styles at the end of the <style>:

nav {
    padding: 30px;

    a {
        font-weight: bold;
        color: inherit;
        text-decoration: none;

        &.router-link-exact-active {
            color: #42b983;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

In case you are wondering that these styles look familiar to default navigation styles when we create a new Vue app. Then you are correct, I have just changed the default color of the <a> tag.

Now, in the browser if we guess a word and navigate to the guessedWords we will see it there:

Guessed Words functionality Done

Top comments (0)