Tuesday, December 10

Create a Typing Speed Effect with VueJS – Part 2: Timer and Score Board

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.

  1. There should be a 60 second timer that will count down whenever a user is typing.
  2. When the time reaches 0 (Zero), the typing area should be blured, and the typing speed calculated.
  3. The metrics for calculating the typing speed for a user is Number of correct words typed in a minute + typos.
  4. We will also give the user the number of typos they made as a single number.
  5. 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.

  1. When a user starts typing, the timer will start counting down.
  2. 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 speed
  • startTimer: will be used to start the timer
  • endTypingSpeed: 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.

  1. 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.
  2. 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.

  1. Whenever there’s an endTypingSpeed event, we calculate the scores that particular session.
  2. We need to also collect the typos, so this will happen during typing, and will be save in a variable.
  3. 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:

  1. Someone Copies and Pastes the content directly into the text area ¯_(ツ)_/¯
  2. Someone Opens the dev tools to increase the timer
  3. 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

0 0 votes
Article Rating
Subscribe
Notify of
guest

0 Comments
Inline Feedbacks
View all comments
0
Would love your thoughts, please comment.x
()
x