Introduction
In Part 1 – Create a Typing Speed Effect with VueJS We saw how to create a Typing Speed Effect. We are going to extend what we built previously, and include a timer and a score board, so that users can type and see the times they typed in faster.
In order to follow along, you must have read through part 1.
Logic
Since we already have the typing effect covered, we need to do the following.
- There should be a 60 second timer that will count down whenever a user is typing.
- When the time reaches 0 (Zero), the typing area should be blured, and the typing speed calculated.
- The metrics for calculating the typing speed for a user is Number of correct words typed in a minute + typos.
- We will also give the user the number of typos they made as a single number.
- We will then list the leading scores every time a session is ended.
The Complete App will look something close to this. Here’s a live link
Project Setup
Since this is a continuation from part 1, I created a repository that starts at the stage we left part 1 in.
Clone this repository. https://github.com/gangachris/vue-typer, and run the existing app.
git clone https://github.com/gangachris/vue-typer
cd vue-typer
npm install
npm start
The repository has some slight modifications from where we left part 1. We now have a package.json, which installs httpster, and adds a start script npm start
to start the app.
{
"name": "vuetyper",
"version": "1.0.0",
"main": "script.js",
"scripts": {
"start": "httpster"
},
"license": "MIT",
"devDependencies": {
"httpster": "^1.0.3"
}
}
At this point, after running npm start
, you should see the following in your browser when you visit localhost:3333
.
You can type in the text area to see the typing speed and typos effect. We explained how this was achieved in part 1 of this article.
Timer UI
We’re going to add a digital timer, which is just a countdown. Let’s add some html where the timer will be.
First of all add some styling.
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<style>
<!-- code commented out for brevity -->
.timer {
font-size: 100px;
text-align: center;
color: white;
border-radius: 100;
background-color: green;
}
</style>
</head>
<!-- code commented out for brevity -->
</html>
The styles above adds a timer
which will style our timer div.
Next, let’s add in the relevant html for the timer.
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<!-- code commented out for brevity -->
</head>
<body>
<div id="app"
class="container mt-2">
<h1>{{ title }}</h1>
<div class="row">
<div class="col-8">
<!-- code commented out for brevity -->
<div class="typer mt-3">
<!-- code commented out for brevity -->
</div>
</div>
<div class="col-4">
<div class="timer">
60
</div>
</div>
</div>
</div>
<!-- code commented out for brevity -->
</body>
</html>
We’ve added a <div class="col-4">
below the div
that has the paragraph and the typing effect. We’ve also added the the class that we added earlier: .timer
. Refreshing the page should on the browser show this.
Timer Logic
Here’s the logic we are trying to achieve.
- When a user starts typing, the timer will start counting down.
- The timer and the text area should be reset when countdown ends.
Countdown Timer
Let’s add the countdown logic.
We’ll start by adding a state variable called timer
.
script.js
new Vue({
el: '#app',
data: {
title: 'Vue Typer',
originalText: PARAGRAPH,
typedText: '',
typoIndex: -1,
timer: 60 // add timer state variable
}
})
/// code commented out for brevity
Next bind the timer to the html.
<!-- code commented out for brevity-->
<div class="col-4">
<div class="timer">
{{timer}}
</div>
</div>
Next, we’ll use the JavaScript function setInterval, which allows us to run a function at intervals.
The setInterval() method of the WindowOrWorkerGlobalScope mixin repeatedly calls a function or executes a code snippet, with a fixed time delay between each call. It returns an interval ID which uniquely identifies the interval, so you can remove it later by calling clearInterval(). This method is offered on the Window and Worker interfaces.
script.js
// code commented out for brevity
new Vue({
el: '#app',
// code commented out for brevity
methods: {
startTypingSpeed: function () {
// start the typing speed
},
startTimer: function () {
setInterval(function () {
if (this.timer === 0) {
this.endTypingSpeed()
return;
}
this.timer--
}, 1000)
},
endTypingSpeed: function () {
// end the typing speed
},
reset: function() {
// reset everything
}
}
})
We have three methods.
startTypingSpeed
: will be used to start the typing speedstartTimer
: will be used to start the timerendTypingSpeed
: will be used to end the typing speed
We can now trigger the timer to start when we start typing. To do this, we’ll add another state variable called typing
which is a boolean representing whether we have started typing.
new Vue({
el: '#app',
data: {
// code commented out for brevity
typing: false
}
})
/// code commented out for brevity
NOTE While making this, I ran into a bug that seasonal javascript developers might be aware of. If you look at this part of the code.
startTimer: function () {
setInterval(function () {
if (this.timer === 0) {
this.endTypingSpeed()
return;
}
this.timer--
}, 1000)
},
Notice we have a the main function startTimer
, and another function inside the setInterval
. This means that the second function creates it’s own this
variable, and therefore replaces the VueJS instance this
.
There are two ways to solve this problem.
- use
var self = this
– this allows us to save the Vue instance this into a new variable, and since objects are passed by reference in JavaScript, everything will work fine. - use arrow functions
This is from Mozilla Developer Network on arrow functions
An arrow function does not have its own this; the this value of the enclosing execution context is used. Thus, in the following code, the this within the function that is passed to setInterval has the same value as this in the enclosing function:
function Person(){
this.age = 0;
setInterval(() => {
this.age++; // |this| properly refers to the Person object
}, 1000);
}
var p = new Person();
I decide to go with arrow functions. This is the new script.js
const PARAGRAPH = "..."
new Vue({
el: '#app',
data: {
// code commented out for brevity
typing: false,
timerInterval: {} // new timerInterval state variable to allow clearInterval
},
methods: {
// we start the typing speed here by changing typing to true
// and starting the timer
startTypingSpeed: function() {
this.typing = true
this.startTimer()
},
startTimer: function() {
// replace the timer function to use arrow functions
// because arrow functions use a `this` that belongs to the
// context wrapping it.
this.timerInterval = setInterval(() => {
if (this.timer === 0) {
this.endTypingSpeed()
return
}
this.timer--
}, 1000)
},
endTypingSpeed: function() {
// end the typing speed
// change typing back to false and
// blur the typing area
clearInterval(this.timerInterval);
this.typing = false;
this.timer = 60;
document.activeElement.blur();
},
reset: function() {
// reset everything
clearInterval(this.timerInterval);
this.typing = false;
this.typoIndex = -1;
this.typedText = '';
this.timer = 60;
}
},
computed: {
outputHTML: function() {
// code commented out for brevity
}
},
watch: {
typedText: function(value) {
// check if already typing, else start the timer.
if (!this.typing) {
this.startTypingSpeed()
}
for (let i = 0; i < value.length; i++) {
if (value[i] !== this.originalText[i]) {
this.typoIndex = i;
break;
}
this.typoIndex = -1;
}
}
}
});
I’ve made comments on every new thing changed in the codebase.
Another change that needs to be explained is addition of another state variable timerInterval
. This is then assigned the setInterval
function value. The reason for this is so that at the end we can use clearInterval to stop the interval function from running forever.
If we run the app again, and start typing, we’ll see the timer counting down.
The GIF is a bit slow, but you get the idea.
The final step here is to add a reset button, so that a user can start a bew session. The logic for this has already been implemented in the reset
function.
reset: function() {
// reset everything
clearInterval(this.timerInterval);
this.typing = false;
this.typoIndex = -1;
this.typedText = '';
this.timer = 60;
}
So we only need to add in the button. Let’s add it on top of the timer.
index.html
<div class="col-4">
<button class="btn btn-primary btn-block" @click="reset()">Reset</button>
<div class="timer mt-3">
{{ timer }}
</div>
</div>
VueJS uses v-on
directive to register events. It has a short hand @
which can be used too, and that is what we’ve used here. The rest is standard CSS buttons.
The page should now look like below
.
You can start typing, and test it out.
Score Board UI
Since the idea is that you can type as many times as you want, we’re adding in a score board.
Add the following html, below the timer div.
<div class="scoreboard mt-3">
<h2>Scoreboard</h2>
<div class="scores">
<div class="alert-info mt-2">1. 20 WPM, 45 Typos </div>
<div class="alert-info mt-2">2. 20 WPM, 45 Typos </div>
<div class="alert-info mt-2">3. 20 WPM, 45 Typos </div>
</div>
</div>
The page should now look like this. Forgive my design skills 😉
Score Board Logic
Here’s the logic for the Score Board.
- Whenever there’s an endTypingSpeed event, we calculate the scores that particular session.
- We need to also collect the typos, so this will happen during typing, and will be save in a variable.
- We then rank the scores on the left, depending on the number of times the user plays.
First of all, let’s add in a score state variable, and a typos state variable, since we will be counting typos, which will be an array.
new Vue({
el: '#app',
data: {
// code commented out for brevity
typos: 0,
scores: []
}
})
Next, let’s count the typos in the typedText
watcher.
// code commented out for brevity
watch: {
typedText: function(value) {
if (!this.typing) {
this.startTypingSpeed();
}
for (let i = 0; i < value.length; i++) {
if (value[i] !== this.originalText[i]) {
this.typoIndex = i;
this.typos++; // increase the typo count
break;
}
this.typoIndex = -1;
}
}
}
// code commented out for brevity
Withing the loop, when we find a typo we increase the typo value by one.
Next, We’ll get the total number of correct values typed. The logic here is to get the typoIndex
and use that to calculate the number of correct words typed. We’ll add in a method called calculateScore
. Add this under the methods property of the VueJS instance.
calculateScore: function() {
let score = {};
let correctlyTypedText = this.typedText
if (this.typoIndex != -1) {
correctlyTypedText = this.originalText.substring(0, this.typoIndex);
}
let words = correctlyTypedText.split(' ').length;
score = {
wpm: words,
typos: this.typos
};
// reset typos
this.typos = 0;
this.scores.push(score);
}
We get the correctly typed text by taking a substring of the original text, and the typo index, then we spit it by spaces to get the words with split method for javascript strings. If we do not have a typo, then we take the existing typed in text.
We then just do a count, and create a score object, which we push into the existing scores state variable. this.scores
. Remember to reset the typos back to 0.
Next, add this function after the endTypingSpeed function is called, within the startTimer function.
startTimer: function() {
this.timerInterval = setInterval(() => {
if (this.timer === 0) {
this.endTypingSpeed();
this.calculateScore(); // calculate the typing speed
return;
}
this.timer--;
}, 1000);
},
We now have to display the typing speed result on the score board. We do this by using a v-for
directive like shown below.
<div class="scoreboard mt-3">
<h2>Scoreboard</h2>
<div class="scores">
<div class="alert-info mt-2"
v-for="(score, index) in scores">{{index + 1}}. {{score.wpm}} WPM, {{score.typos}} Typos </div>
</div>
</div>
v-for
directive for vue allows us to loop over lists, and can take the form of v-for="item in items"
, or v-for="(item, index) in items"
when we need to access the indices.
Run the app, and type in a few times, you should see the results.
The last step for this article is to sort/rank the results in the score board. We’ll do this again using computed properties, so that the logic is clear. Add a computed property to the VueJS instance
// code commented out for brevity
computed: {
outputHTML: function() {
// code commented out for brevity
},
sortedScores: function() {
return this.scores.sort((a, b) => {
return a.wpm + a.typos - (b.wpm + b.typos);
});
}
},
// code commented out for brevity
Then replace the scores
list in the html to use sortedScores
<div class="alert-info mt-2"
v-for="(score, index) in sortedScores">{{index + 1}}. {{score.wpm}} WPM, {{score.typos}} Typos </div>
The app should be running well if you check it in the browsers.
Bugs/Cheats
Like any other game out there, there are a couple of ways people can cheat the game. Here are a few:
- Someone Copies and Pastes the content directly into the text area ¯_(ツ)_/¯
- Someone Opens the dev tools to increase the timer
- If you play about 100 times, the list will get long and messy.
We are not really worried about this, because it’s intended to run on a local machine. Can you think of ways to stop this, and leave a comment.
Conclusion
VueJS brings with it the purity of vanilla javascript and a bit of helpful tools. You can see that apart from the bindings, most of the code written is pure Javascript, with line by line thinking.
The full code can be found in the complete branch of the repository.
I hope you enjoyed this, and Happy Coding.
Source: Scotch.io