1. Table of Contents
- 1. Table of Contents
- 2. Introduction
- 3. Requirements
- 4. Creating the project
- 5. Initial Setup
- 6. Starting Up
- 7. Making the Game
- 8. Additional Stuff
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
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
It will ask you about a preset:
Select manually and hit enter.
Next it will ask about what features do we want:
For our project we will be using typescript, scss, vue-router, and eslint. So select the following and hit enter.
Next it will ask which version of vue do we want to use:
Select 3.x(Preview) and hit enter.
Next, it will ask us a couple of yes/no questions. Answer as following:
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:
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 .
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
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:
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
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');
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>
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;
}
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>
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>
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:
- A random word (we are going to use the random-words package for this)
- Show visually how many letters are in the word.
- Functionality to enter letters one by one.
- If the letter is correct then show it in its place.
- Else if the letter is wrong then it joins the list of wrong letters, which we need to show on the screen.
- And the next body part of the stick-man is drawn.
- If a letter is entered again then we need to show a notification saying letter has already been entered.
- 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.
- 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.
- On the popup we also need to have a play again button.
- 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.
- 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';
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('');
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;
Going ahead , now we are going to import the random-words
at the top:
import randomWord from 'random-words';
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;
};
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;
};
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);
};
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();
}
};
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 nowunmounted
& -
beforeDestroy
is nowbeforeUnmount
.
We are going to register three lifecycle hooks:
-
onBeforeMount
: Here we are going to add the eventListener, for keyup, on thewindow
. -
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';
Next we are going to call these functions to register the hooks:
onBeforeMount(() => window.addEventListener('keydown', keyDown));
onMounted(() => play());
onUnmounted(() => window.removeEventListener('keydown', keyDown));
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,
};
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>
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>
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;
}
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');
},
},
});
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" />
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,
},
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>
Now, if we look in the browser we will just see the hanging pole:
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;
}
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,
},
},
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';
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);
},
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;
});
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');
}
});
At the end we need to return the computed property letters
:
return {
letters,
};
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>
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
},
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')" />
Now, in the browser it will show us the dashes for every letter:
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:
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:
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,
},
},
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>
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;
}
}
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,
},
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" />
This is it for this component.
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,
},
},
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%;
}
}
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>
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
},
Now, in the template after the <main>
tag add the component and bind the show
prop to the notification
variable:
<letter-notification :show="notification" />
Now, in the browser if we enter a letter again it will show us notification and a second later it is going to disappear:
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');
},
},
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;
}
}
}
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>
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,
},
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>
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.
And if we click the play again button the game will restart:
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";
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);
},
};
At the end export the store
as the default export for the file:
export default store;
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';
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');
}
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
},
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>
Copy & paste the following styles in the <style>
tag:
li {
list-style-type: none;
font-weight: 600;
}
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;
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'),
},
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>
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;
}
}
}
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:
Top comments (0)