Building a Quiz App in JavaScript
Tags: javascript, tutorial, webdev, codenewbie
Published: 2020-09-22 13:25:41

My goal is to take you on a trip from planning to building a quiz application. For that, we will use vanilla JavaScript, CSS, and HTML. No additional libraries or packages. Let’s get started by defining what our quiz app can do.

### Defining the specification
The quiz will be split into two main classes. The first one will be a settings area in which the player can choose the difficulty, the category, and the number of questions he wants to answer. For that, we will create a *settings-class* to track all of this information. After doing that he can start the quiz.

The second area will be a quiz. The *quiz-class* tracks the progress of the player and decides whether or not to display the next question of the final screen.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/3h5vrl9bmr7eq48rquit.png)

Furthermore, the *quiz-class* has two other components, first of an array of *question-classes* that hold the data of a question, display it, and checks if the answer was right or not. The other one is the *final-class* that displays the last page with the player’s score.

We will be using the [Open Trivia DB API](https://opentdb.com/api_config.php) for the questions so that we don’t have to come up with our own questions.

As you might already guess, because I am talking a lot about classes, we will use [Object-Oriented-Programming](https://en.wikipedia.org/wiki/Object-oriented_programming) to implement this quiz app and not [Functional-Programming](https://en.wikipedia.org/wiki/Functional_programming). If you are interested in the difference between these two paradigms go an check out my article [“Functional Programming vs OOP in JavaScript”](https://dev.to/klamserdev/oop-vs-functional-programming-39ij).

### Prerequisites
Before we can start implementing the quiz we need to create our folder structure and the HTML and CSS. In this article, we will be focusing on the JavaScript part of the application. Therefore I will be providing the necessary HTML and CSS in this section. Let’s start by creating the folder structure like this:
```Shell
$ mkdir vanilla-quiz
$ cd ./vanilla-quiz
$ mkdir quiz
$ touch index.html index.js styles.css
```
Go ahead and copy and paste the index.html and styles.css from these sources:
```HTML
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Vanilla Quiz</title>
  <link rel="stylesheet" href="styles.css">
  <link href="https://fonts.googleapis.com/css2?family=Questrial&display=swap" rel="stylesheet">
</head>
<body>
  <main>
    <div class="header">
      <h2>Vanilla Quiz</h2>
    </div>
    <div class="main">
      <div class="final">
        <h3>You answered all of the questions!</h3>
        <p>Score: </p><p class="score"></p>
        <h4>Want to try it again?</h4>
        <button id="again" class="submit">Again</button>
      </div>
      <div class="quiz">
        <div class="count">
          <p class="current">0</p><p style="margin-left:40px"> / </p><p class="total"></p>
        </div>
        <h3 id="question"></h3>
        <label id="a1" class="container">
          <input type="radio" checked="checked" name="radio">
          <span class="checkmark"></span>
        </label>
        <label id="a2" class="container">
          <input type="radio" name="radio">
          <span class="checkmark"></span>
        </label>
        <label id="a3" class="container">
          <input type="radio" name="radio">
          <span class="checkmark"></span>
        </label>
        <label id="a4" class="container">
          <input type="radio" name="radio">
          <span class="checkmark"></span>
        </label>
        <button id="next" class="submit">Submit</button>
      </div>
      <div class="settings">
        <h3 style="text-align: center;">Set up your Quiz!</h3>
        <label for="category">Category</label>
        <select name="category" id="category">
          <option value="9">General Knowledge</option>
          <option value="27">Animals</option>
          <option value="15">Video Games</option>
          <option value="23">History</option>
          <option value="21">Sports</option>
        </select>
        <div class="mt30">
          <label for="difficulty">Difficulty</label>
          <label class="container" style="display: inline; margin-left: 30px;">Easy
            <input type="radio" name="radio" id="easy">
            <span class="checkmark" style="margin-top: 2px;"></span>
          </label>
          <label class="container" style="display: inline; margin-left: 30px;">Medium
            <input type="radio" name="radio" id="medium">
            <span class="checkmark" style="margin-top: 2px;"></span>
          </label>
          <label  class="container" style="display: inline; margin-left: 30px;">Hard
            <input type="radio" name="radio" id="hard">
            <span class="checkmark" style="margin-top: 2px;"></span>
          </label>
        </div>
        <div class="mt30">
          <label for="questions">Number of questions</label>
          <input name="questions" id="questions" type="text" pattern="[0-9]*" />
        </div>  
        <button id="start" class="submit">Start</button>
      </div>  
    </div>
  </main>
  <script type="module" src="index.js"></script>
</body>
</html>
```
```CSS
:root {
  --primary-color: #5D737E;
  --secondary-color: #D6F8D6;
  --tertiary-color: #7FC6A4;
  --quaternary-color: #55505C;
  --hover-color: #4e616b;
  --shadow-color:rgba(57, 127, 93, 0.4);
  --font-style: 'Questrial';
}

body {
  font-family: var(--font-style), 'Ranchers', cursive;
  background-color: var(--secondary-color);
  width: 100vw;
  height: 100vh;
  justify-content: center;
  align-items: center;
}

h2 {
  font-size: 3.5rem;
  text-align: center;
  color: var(--primary-color);
}

.mt30 {
  margin-top: 30px;
}

.header {
  padding: 15px;
}

.main {
  display: flex;
  justify-content: center;
}

.settings {

  z-index: 1;
}

.final {
  visibility: hidden;
  z-index: 2;
}

.final p {
  font-size: 30px;
  text-align: center;
}

.final h4 {
  font-size: 33px;
  text-align: center;
}

.quiz  {
  visibility: hidden;
  z-index: 0;
}

#questions {
  font-size: 20px;
  font-family: var(--font-style), 'Ranchers', cursive;
  font-weight: 600;
  line-height: 1.3;
  color: white;
  background-color: var(--primary-color);
  appearance: none;
  border: none;
  padding: 5px;
  border-radius: 5px;
  margin-left: 30px;
  outline: none;
  text-align: center;
  width: 120px;
}
.settings select {
  font-size: 20px;
  font-family: var(--font-style), 'Ranchers', cursive;
  font-weight: 600;
  line-height: 1.3;
  letter-spacing: 1px;
  color: white;
  background-color: var(--primary-color);
  -moz-appearance: none;
  -webkit-appearance: none;
  appearance: none;
  border: none;
  padding: 5px;
  border-radius: 5px;
  margin-left: 20px;
  outline: none;
  text-align: center;
}

.settings select::-ms-expand {
  display: none;
}

.settings select:hover {
  border-color: var(--hover-color);
}

.settings select:focus {
  border-color: var(--hover-color);
}

.settings select option {
  /* font-weight: bolder; */
  font-family: var(--font-style), 'Ranchers', sans-serif;
}

.settings label {
  font-size: 25px;
  margin-right: 16px;
}


.quiz, .settings, .final {
  position: absolute;
  padding: 0px 35px 35px 35px;
  max-width: 560px;
  background-color: var(--tertiary-color);
  border-radius: 7px;
  -webkit-box-shadow: 10px 10px 3px -4px var(--shadow-color);
  -moz-box-shadow: 10px 10px 3px -4px var(--shadow-color);
  box-shadow: 10px 10px 5px -4px var(--shadow-color);
}

h3 {
  display: block;
  width: 550px;
  font-size: 35px;
  font-weight: 350;
  word-wrap: break-word;
}

.submit {
  width: 100%;
  color: white;
  background-color: var(--primary-color);
  font-family: var(--font-style), 'Ranchers', cursive;
  outline: none;
  border: none;
  height: 50px;
  font-size: 1.8rem;
  margin-top: 20px;
  border-radius: 5px;
  letter-spacing: 2px;
}

.submit:hover {
  background-color: var(--hover-color);
  cursor: pointer;
  color: #FAF33E;
}

/* The container */
.count {
  display: block;
  left: 75%;
  position: relative;
  padding-left: 35px;
  margin-bottom: 100px;
  cursor: pointer;
}

.count p {
  position: absolute;
  font-size: 35px;
  
}

.total {
  margin-left: 50px;
}

/* The container */
.container {
  display: block;
  position: relative;
  padding-left: 35px;
  margin-bottom: 12px;
  cursor: pointer;
  font-size: 25px;
  -webkit-user-select: none;
  -moz-user-select: none;
  -ms-user-select: none;
  user-select: none;
}

/* Hide the browser's default radio button */
.container input {
  position: absolute;
  opacity: 0;
  cursor: pointer;
}

/* Create a custom radio button */
.checkmark {
  position: absolute;
  top: -2px;
  left: 0px;
  height: 25px;
  width: 25px;
  background-color: white;
  border-radius: 30%;
}

/* On mouse-over, add a grey background color */
.container:hover input ~ .checkmark {
  background-color: #FAF33E;
}

/* When the radio button is checked, add a blue background */
.container input:checked ~ .checkmark {
  background-color: var(--quaternary-color);
}

/* Create the indicator (the dot/circle - hidden when not checked) */
.checkmark:after {
  content: "";
  position: absolute;
  display: none;
}

/* Show the indicator (dot/circle) when checked */
.container input:checked ~ .checkmark:after {
  display: block;
}
```
Now we are ready to start working on our app. The first class we will be working on is **Settings**.

### How to get the questions?
The goal of the *settings-class* is that the player can choose their category, difficulty, and the number of questions they want to answer. We need to create a request to the Open Trivia DB API out of these three parameters to get the questions for the player’s playthrough.

Before we start creating our class we need to create an instance of it in the *index.js* like this:
```JavaScript
import Settings from ‘./quiz/settings.js’;

new Settings();
```
This will give an error because the file *settings.js* does not exist yet, so let’s go ahead and create it.
```Shell
$ touch ./quiz/settings.js
```
Next up we create the skeleton for our *settings-class*. For that, we need the class with a constructor and a *startQuiz-method* plus the *export-statement*. Without an *export-statement*, we could not import the class in *index.js*. This is how it should look like:
```JavaScript
class Settings {
  constructor() {
  }
  
  startQuiz() {
  }
}

export default Settings;
```
In the constructor, we want to get all the DOM-Elements we need to start a quiz. For that, we need to grab the two div’s, *quiz*, and *settings* to toggle their visibility when the player wants to start the quiz. Next up we need all the parameters so that we can create the request for getting the questions. Last but not least we need to get the button to add the *startQuiz-method* to the *click-event*.
```JavaScript
constructor() {
  this.quizElement = document.querySelector('.quiz');
  this.settingsElement = document.querySelector('.settings');
  this.category = document.querySelector('#category');
  this.numberOfQuestions = document.querySelector('#questions');
  this.difficulty = [
    document.querySelector('#easy'),
    document.querySelector('#medium'),
    document.querySelector('#hard'),
  ];
  this.startButton = document.querySelector('#start');
  
  this.quiz = { };  
  this.startButton.addEventListener('click', this.startQuiz.bind(this));
}
```
In the first segment, we get all the DOM-Elements, note that we store the elements for the difficulty in an array to filter them later on. After that, we initialize the *quiz-property* and add the *startQuiz-method* to the *startButton*. Be sure to bind **this** to the *startQuiz-method*. If you don’t do that you won’t have **this** available inside the method.

For the quiz to start we need to collect all the parameters and create the request dynamically. Because we are dealing with an API-call I decided to use *async/await* to handle the asynchronous call. To make sure no error gets lost we will wrap the whole call in a *try-catch-block*. So the *startQuiz-method* should look something like this:
```JavaScript
async startQuiz() {
  try {
    const amount = this.getAmount();
    const categoryId = this.category.value;
    const difficulty = this.getCurrentDifficulty();
 
    const url = `https://opentdb.com/api.php?amount=${amount}&category=${categoryId}&difficulty=${difficulty}&type=multiple`;
    let data = await this.fetchData(url);
    this.toggleVisibility();
    this.quiz = new Quiz(this.quizElement, amount, data.results);
  } catch (error) {
    alert(error);
  }
}
```
What are we doing here?

First, we get all three values, for the *amount* and *difficulty* we use methods that are not yet implemented. In these methods, we will be handling errors e.g. not choosing any difficulty or entering a negative number for the number of questions.

After that, we create the URL with the parameters we just got. This URL is passed in the *fetchData-method* which will send the request and returns the data. After that, we call *toggleVisibility* and initialize a new *quiz-object* by passing in the *result*, *amount*, and the *quizElement*.

If at any point an error is thrown we will catch it and display it by using the *alert-method*.

The final *settings-class* should look like this:
```JavaScript
import Quiz from './quiz.js';

class Settings {
  constructor() {
    this.quizElement = document.querySelector('.quiz');
    this.settingsElement = document.querySelector('.settings');
    this.category = document.querySelector('#category');
    this.numberOfQuestions = document.querySelector('#questions');
    this.difficulty = [
      document.querySelector('#easy'),
      document.querySelector('#medium'),
      document.querySelector('#hard'),
    ];
    this.startButton = document.querySelector('#start');

    this.quiz = { };

    this.startButton.addEventListener('click', this.startQuiz.bind(this));
  }

  async startQuiz() {
    try {
      const amount = this.getAmount();
      const categoryId = this.category.value;
      const difficulty = this.getCurrentDifficulty();

      const url = `https://opentdb.com/api.php?amount=${amount}&category=${categoryId}&difficulty=${difficulty}&type=multiple`;

      let data = await this.fetchData(url);
      this.toggleVisibility();
      this.quiz = new Quiz(this.quizElement, amount, data.results);
    } catch (error) {
      alert(error);
    }
  }

  toggleVisibility() {
    this.settingsElement.style.visibility = 'hidden';
    this.quizElement.style.visibility = 'visible';
  }

  async fetchData(url) {
    const response = await fetch(url);
    const result = await response.json();

    return result;
  }

  getCurrentDifficulty() {
    const checkedDifficulty = this.difficulty.filter(element => element.checked);

    if (checkedDifficulty.length === 1) {
      return checkedDifficulty[0].id;
    } else {
      throw new Error('Please select a difficulty!');
    }
  }

  getAmount() {
    const amount = this.numberOfQuestions.value;
    // Not negative, not 0 and not over 50
    if (amount > 0 && amount < 51) {
      return amount;
    }
    throw new Error('Please enter a number of questions between 1 and 50!');
  }
}

export default Settings;
```
Both methods *getAmount* and *getCurrentDifficulty* are returning an error if the player did not select anything or the selected value is out of bounds (for the number of questions). We also added the *import-statement* for the *quiz-class* at the top of this file. The other two methods (*fetchData* and *toggleVisibility*) do exactly what their names suggest. Now we can focus on the *quiz-class* next.

### It's Quiz Time!
Before we start thinking about the *quiz-class* we need to create a file that will contain it.
```Shell
$ touch ./quiz/quiz.js
```
We start as we did with *settings.js* by creating a skeleton.
```JavaScript
class Quiz {
  constructor(quizElement, amount, questions) {
    this.quizElement = quizElement;
    this.totalAmount = amount;
    this.questions = this.setQuestions(questions);
  }
  setQuestions(questions) {
    return questions.map(question => new Question(question));
  }
  nextQuestion() {
  }
  endQuiz() {
  }
}
export default Settings;
```
This time we have some arguments that got passed in by the *settings-object* that we need to deal with. For the *questions*, we create a single *question-object* for each question that got passed in by the *settings-object*. The *constructor* needs some more set up so we will add some more DOM-Elements and an *event-listener* to the *nextButton* too. So let’s go ahead and do this!
```JavaScript
constructor(quizElement, amount, questions) {
  this.quizElement = quizElement;
  this.currentElement = document.querySelector('.current');
  this.totalElement = document.querySelector('.total');
  this.nextButton = document.querySelector('#next');
  this.finalElement = document.querySelector('.final')
  this.totalAmount = amount;
  this.answeredAmount = 0;
  this.questions = this.setQuestions(questions);
  this.nextButton.addEventListener('click',
  this.nextQuestion.bind(this));
  this.renderQuestion();
}
```
As you can see it almost looks like the constructor in *settings.js*. One major difference is the call of *renderQuestion* at the end. The purpose of this call is that we want to render the first question immediately.

Between *setQuestions* and *nextQuestion* we create the method renderQuestion and implement it like this:
```JavaScript
renderQuestion() {
  this.questions[this.answeredAmount].render();
  this.currentElement.innerHTML = this.answeredAmount;
  this.totalElement.innerHTML = this.totalAmount;
}
```
At the start of the quiz, the *answeredAmount* is 0 so we call the *render-method* on the first question in the *questions-array*. After that, we set the current progress of the player. Because we did not implement the *question-class* yet this code throws an error, but we will fix this soon.

Let’s implement the *nextQuestion-method*. For that, if the player checked an answer and if so, which answer was checked. Next up we need to show the result to the player and increment the *answeredAmount* by one. Finally, we have to check if there is another question left and if so display it. If this was the last one we need to proceed to the result screen.
```JavaScript
nextQuestion() {
  const checkedElement = this.questions[this.answeredAmount].answerElements.filter(el => el.firstChild.checked);
  if (checkedElement.length === 0) {
    alert(‘You need to select an answer’);
  } else {
    this.questions[this.answeredAmount].answer(checkedElement)
    this.showResult();
    this.answeredAmount++;
    (this.answeredAmount < this.totalAmount) ? this.renderQuestion() : this.endQuiz();
  }
}
```
The only methods that are missing in this class are *showResult*, *endQuiz*, and a method to sum up all the correct answers for the result screen. The final *quiz.js* should look like this:
```JavaScript
import Final from './final.js';
import Question from './question.js'

class Quiz {
  constructor(quizElement, amount, questions) {
    this.quizElement = quizElement;
    this.currentElement = document.querySelector('.current');
    this.totalElement = document.querySelector('.total');
    this.nextButton = document.querySelector('#next');
    this.finalElement = document.querySelector('.final')

    this.totalAmount = amount;
    this.answeredAmount = 0;
    this.questions = this.setQuestions(questions);
    
    this.nextButton.addEventListener('click', this.nextQuestion.bind(this));
    this.renderQuestion();
  }

  setQuestions(questions) {
    return questions.map(question => new Question(question));
  }

  renderQuestion() {
    this.questions[this.answeredAmount].render();
    this.currentElement.innerHTML = this.answeredAmount;
    this.totalElement.innerHTML = this.totalAmount;
  }

  nextQuestion() {
    const checkedElement = this.questions[this.answeredAmount].answerElements.filter(el => el.firstChild.checked);
    if (checkedElement.length === 0) {
      alert('You need to select an answer');
    } else {
      this.questions[this.answeredAmount].answer(checkedElement)
      this.showResult();
      this.answeredAmount++;
      (this.answeredAmount < this.totalAmount) ? this.renderQuestion() : this.endQuiz();
    }
  }

  showResult() {
    this.questions[this.answeredAmount].isCorrect ? alert('Correct answer :)') : alert('Wrong answer :(');
  }

  endQuiz() {
    this.quizElement.style.visibility = 'hidden';
    this.finalElement.style.visibility = 'visible';
    const correctAnswersTotal = this.calculateCorrectAnswers();
    this.final = new Final(correctAnswersTotal, this.totalAmount);
  }

  calculateCorrectAnswers() {
    let count = 0;
    this.questions.forEach(el => {
      if (el.isCorrect) {
        count++;
      }
    });
    return count;
  }
}

export default Quiz;
```
We added the two imports at the top for *question.js* and *final.js*. Additionally, we implemented *showResult* by checking if the question was correctly answered with the [ternary operator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Conditional_Operator).

The *endQuiz-method* looks a little bit like the *toggleVisibility-method* from our *settings.js*, except that it sums up all the correct answers by calling *calculateCorrectAnswers* and then passing it to a new instance of the *final-class* (we still need to implement that class).

### Displaying a Question and the Result
Our *quiz-class* does not work at the moment because two dependencies do not exist yet. Let’s change this by adding the two files like this:
```Shell
$ touch ./quiz/question.js ./quiz/final.js
```
We start by implementing the *question-class*. First of all, we add a skeleton to the file like this:
```JavaScript
class Question {
  constructor(question) {
    this.correctAnswer = question.correct_answer;
    this.question = question.question;
    this.answers = this.shuffleAnswers([
      question.correct_answer,
      ...question.incorrect_answers
    ]);
  }
  shuffleAnswers(answers) {
  }
  
  answer(checkedElement) {
  }
  render() {
  }
}
export default Question;
```
So what did we do here?

We save the question, correct answer, and an array of answers that we shuffle before we store it.

The next step is to implement the *shuffleAnswers*, *answer*, and *render* methods. For the shuffling of the array, we will use the [Fisher-Yates-Shuffle-Algorithm](https://www.geeksforgeeks.org/shuffle-a-given-array-using-fisher-yates-shuffle-algorithm/).

The *answer-method* will just compare the choice of the player with the *correctAnswer* property and the *render* method will display the question and all the possible answers. For this to work, we need to get the respective DOM-Elements and end up with this *question.js*:
```JavaScript
class Question {
  constructor(question) {
    this.questionElement = document.querySelector('#question');
    this.answerElements = [
      document.querySelector('#a1'),
      document.querySelector('#a2'),
      document.querySelector('#a3'),
      document.querySelector('#a4'),
    ];

    this.correctAnswer = question.correct_answer;
    this.question = question.question;
    this.isCorrect = false;

    this.answers = this.shuffleAnswers([
      question.correct_answer, 
      ...question.incorrect_answers
    ]);
  }

  shuffleAnswers(answers) {
    for (let i = answers.length - 1; i > 0; i--){
      const j = Math.floor(Math.random() * i)
      const temp = answers[i]
      answers[i] = answers[j]
      answers[j] = temp
    }
    return answers;
  }

  answer(checkedElement) {
     this.isCorrect = (checkedElement[0].textContent === this.correctAnswer) ? true : false;
  }

  render() {
    this.questionElement.innerHTML = this.question;
    this.answerElements.forEach((el, index) => {
      el.innerHTML = '<input type="radio" name="radio"><span class="checkmark"></span>' + this.answers[index];
    });
  }
}

export default Question;
```
Now the only thing missing is the *final-class*. This class is really simple we just need to get the DOM-Elements to display the final result to the player. To add some convenience we can add an *again-button* that reloads the page so the player can start again. This is how it should look like:
```JavaScript
class Final {
  constructor(count, totalAmount) {
    this.scoreElement = document.querySelector('.score');
    this.againButton = document.querySelector('#again');

    this.render(count, totalAmount);
    this.againButton.addEventListener('click', location.reload.bind(location));
  }

  render(count, totalAmount) {
    this.scoreElement.innerHTML = `You answered ${count} out of ${totalAmount} correct!`;
  }
}

export default Final;
```

### Conclusion
The quiz app is now complete. We implemented this with just plain old JavaScript and used the concept of Object-Oriented-Programming. I hope you enjoyed this and as always you can find the code on my [GitHub](https://github.com/JakobKIT/vanilla-quiz).

### Screenshots
The players can choose a category, difficulty, and the number of questions they want to answer and then start the quiz.
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/rx4tgy41i80ez595v4i6.png)
When started the multiple-choice questions show up like this.
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/taabslnxo89uq4uv6lth.png)
And at the end of all questions, you get to see this final page.
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/d52w63h597evvyjehv8i.png)

I hope you had fun following along. Try and improve this version if you want to.

<span>Photo by <a href="https://unsplash.com/@emilymorter?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Emily Morter</a> on <a href="https://unsplash.com/s/photos/question-mark?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Unsplash</a></span>