Task 3: Make the Quiz

Step 1: Layout and Styling

First, replace the content of index.html with the following HTML code:

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/ico" href="/favicon.png" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Quiz App</title>
    <link href="/style.css" rel="stylesheet" />
  </head>
  <body
    class="min-h-screen bg-gray-100 py-6 flex flex-col justify-center sm:py-12"
  >
    <div class="relative py-3 sm:max-w-xl sm:mx-auto">
      <div
        class="relative px-4 py-10 bg-white mx-8 md:mx-0 shadow rounded-3xl sm:p-10"
      >
        <div class="max-w-md mx-auto">
          <div class="flex items-center space-x-5">
            <div
              class="block pl-2 font-semibold text-xl self-start text-gray-700"
            >
              <h2 id="quiz-title" class="leading-relaxed">TypeScript Quiz</h2>
            </div>
          </div>
          <div class="divide-y divide-gray-200">
            <div
              id="quiz-container"
              class="py-8 text-base leading-6 space-y-4 text-gray-700 sm:text-lg sm:leading-7"
            >
              <ol class="list-inside list-decimal pl-2">
                <li class="mb-4">
                  <label class="leading-loose">
                    TypeScript is a superset of JavaScript.
                  </label>
                  <div class="flex gap-3 ml-5">
                    <div class="flex">
                      <input type="radio" name="q1" id="q1t" class="mt-1" />
                      <label for="q1t" class="ml-1">True</label>
                    </div>
                    <div class="flex">
                      <input type="radio" name="q1" id="q1f" class="mt-1" />
                      <label for="q1f" class="ml-1">False</label>
                    </div>
                  </div>
                </li>
                <li class="mb-4">
                  <label class="leading-loose">
                    TypeScript cannot be run in a web browser.
                  </label>
                  <div class="flex gap-3 ml-5">
                    <div class="flex">
                      <input type="radio" name="q2" id="q2t" class="mt-1" />
                      <label for="q2t" class="ml-1">True</label>
                    </div>
                    <div class="flex">
                      <input type="radio" name="q2" id="q2f" class="mt-1" />
                      <label for="q2f" class="ml-1">False</label>
                    </div>
                  </div>
                </li>
                <li class="mb-4">
                  <label class="leading-loose">
                    TypeScript was developed by Microsoft.
                  </label>
                  <div class="flex gap-3 ml-5">
                    <div class="flex">
                      <input type="radio" name="q3" id="q3t" class="mt-1" />
                      <label for="q3t" class="ml-1">True</label>
                    </div>
                    <div class="flex">
                      <input type="radio" name="q3" id="q3f" class="mt-1" />
                      <label for="q3f" class="ml-1">False</label>
                    </div>
                  </div>
                </li>
                <li class="mb-4">
                  <label class="leading-loose">
                    Which one can be used to define an enumeration?
                  </label>
                  <div class="flex flex-col ml-5">
                    <div class="flex">
                      <input type="radio" name="q4" id="q4a" />
                      <label for="q4a" class="ml-1">enum Keyword</label>
                    </div>
                    <div class="flex">
                      <input type="radio" name="q4" id="q4b" />
                      <label for="q4b" class="ml-1">enumeration Type</label>
                    </div>
                    <div class="flex">
                      <input type="radio" name="q4" id="q4c" />
                      <label for="q4c" class="ml-1">enum Type</label>
                    </div>
                    <div class="flex">
                      <input type="radio" name="q4" id="q4d" />
                      <label for="q4d" class="ml-1">enumerable Keyword</label>
                    </div>
                  </div>
                </li>
                <li class="mb-4">
                  <label class="leading-loose">
                    Which one is NOT a TypeScript data type?
                  </label>
                  <div class="flex flex-col ml-5">
                    <div class="flex">
                      <input type="radio" name="q5" id="q5a" class="mt-1" />
                      <label for="q5a" class="ml-1">string</label>
                    </div>
                    <div class="flex">
                      <input type="radio" name="q5" id="q5b" class="mt-1" />
                      <label for="q5b" class="ml-1">number</label>
                    </div>
                    <div class="flex">
                      <input type="radio" name="q5" id="q5c" class="mt-1" />
                      <label for="q5c" class="ml-1">boolean</label>
                    </div>
                    <div class="flex">
                      <input type="radio" name="q5" id="q5d" class="mt-1" />
                      <label for="q5d" class="ml-1">character</label>
                    </div>
                  </div>
                </li>
              </ol>
            </div>
            <div class="pt-4 flex items-center">
              <button
                id="submit-btn"
                class="flex justify-center items-center w-full text-white px-4 py-3 rounded-md focus:outline-none"
                style="background-color: #3b82f6"
              >
                Submit
              </button>
            </div>
          </div>
        </div>
      </div>
    </div>
    <script type="module" src="/src/main.ts"></script>
  </body>
</html>

Next, update the main.ts file:

import "./style.css";

Now, run the app:

Step 2: Understanding of Interfaces as Types

To begin, let's create a small sample of quiz questions. We'll start by adding one TrueFalseQuestion and one MultipleChoiceQuestion to the sampleQuestions collection. After that, we can print them to the console to ensure they are structured correctly. Update the main.ts as follows:

import "./style.css";
import { MultipleChoiceQuestion, TrueFalseQuestion, Quiz } from "./model";

const sampleQuestions: Quiz = {
  title: "Sample Quiz",
  questions: [
    {
      id: 1,
      text: "TypeScript is a superset of JavaScript.",
      answer: true,
    } as TrueFalseQuestion,
    {
      id: 2,
      text: "Which one can be used to define an enumeration?",
      options: [
        "enum Keyword",
        "enumeration Type",
        "enum Type",
        "enumerable Keyword",
      ],
      answer: "enum Keyword",
    } as MultipleChoiceQuestion,
  ],
};

// Printing to the console
console.log(sampleQuestions);

Inspect the browser console!

Explanation:

  • Using Interface as a Data Type: By declaring sampleQuestions as Quiz, you are specifying that sampleQuestions must follow the structure defined by the Quiz interface. This provides type safety, ensuring that the object's shape is as expected.
  • Type Assertions (as TrueFalseQuestion): The as TrueFalseQuestion is a type assertion, explicitly instructing TypeScript to treat the object as a TrueFalseQuestion. This is important when dealing with union types or extending base types to ensure that the correct properties and methods are accessed.

Step 3: Type Guarding via Property Checking

After creating and understanding our sample questions, we'll incrementally implement logic to render them. For the sake of understanding, we will initially focus on rendering the text to the console before moving on to rendering in the DOM. This step provides us with a seamless transition into understanding type guarding in TypeScript.

Create a minimal function that will render our question text to the console. This function should map over our sampleQuestions and print them to the console based on their type.

function renderQuestion(question: QuizQuestion) {
  // Print question to the console
  console.log(question.text);

  if ("options" in question) {
    // It’s a MultipleChoiceQuestion, print the options too
    console.log("Options:", question.options);
  } else {
    console.log("Options:", ["True", "False"]);
  }
}

sampleQuestions.questions.forEach((question) => renderQuestion(question));

Inspect the browser console and observe the output!

Explanation:

  • Type Guarding via Property Checking: The line if ('options' in question) is a prime example of a type guard via property checking. Here, we are not ascertaining the object’s instance or its primitive type; instead, we check for the existence of a property within the object. This method is highly suitable when the object is not an instance of a class, as is the case with our question object.

Reasoning Behind Approach:

In our context, our question objects are object literals and are not instances of a class; hence instanceof is not suitable. Also, typeof isn’t the right fit here since we are dealing with complex objects rather than primitive types. Therefore, we opt for property checking, which allows us to efficiently determine the presence of a specific property within an object and thus ascertain its type.

Step 4: Dynamically Render the Quiz

After understanding the concepts of interfaces, type assertions, and type guarding, it's time to put our knowledge into action by rendering our sample questions dynamically to the HTML, making our quiz interactive and user-friendly.

Let’s implement a function that dynamically creates and appends HTML elements to render each question and its options (if any) based on its type.

function renderQuiz(quiz: Quiz) {
  const quizDiv = document.getElementById("quiz-container");
  const quizTitle = document.getElementById("quiz-title");

  if (!quizDiv || !quizTitle) return;

  // Set the quiz title dynamically
  quizTitle.textContent = quiz.title;

  // Clear any existing questions
  while (quizDiv?.firstChild) {
    quizDiv.firstChild.remove();
  }

  // Create ordered list
  const ol = document.createElement("ol");
  ol.className = "pl-2 list-decimal list-inside";

  // Loop through questions
  quiz.questions.forEach((question, index) => {
    // Create list item for each question
    const li = document.createElement("li");
    li.className = "mb-4";

    // Create label for question text
    const label = document.createElement("label");
    label.className = "leading-loose";
    label.textContent = question.text;

    // Append label to list item
    li.appendChild(label);

    // Create container for question options
    const optionsContainer = document.createElement("div");
    optionsContainer.className = question.hasOwnProperty("options")
      ? "flex flex-col ml-5"
      : "flex gap-3 ml-5";

    if ("options" in question) {
      // Multiple Choice Questions
      question.options.forEach((option, i) => {
        const char = String.fromCharCode(97 + i); // 97 is ASCII for 'a'
        const input = createRadioInput(
          `q${question.id}${char}`,
          `q${question.id}`,
          option,
        );
        optionsContainer.appendChild(input);
      });
    } else {
      // True/False Questions
      ["True", "False"].forEach((option) => {
        const input = createRadioInput(
          `q${question.id}${option === "True" ? "t" : "f"}`,
          `q${question.id}`,
          option,
        );

        optionsContainer.appendChild(input);
      });
    }

    // Append optionsContainer to list item
    li.appendChild(optionsContainer);

    // Append list item to ordered list
    ol.appendChild(li);
  });

  // Append ordered list to quiz container
  quizDiv.appendChild(ol);
}

function createRadioInput(
  id: string,
  name: string,
  labelContent: string,
): HTMLDivElement {
  const div = document.createElement("div");
  div.className = "flex";

  const input = document.createElement("input");
  input.type = "radio";
  input.name = name;
  input.id = id;
  input.className = "mt-1";

  const label = document.createElement("label");
  label.htmlFor = id;
  label.className = "ml-1";
  label.textContent = labelContent;

  div.appendChild(input);
  div.appendChild(label);

  return div;
}

renderQuiz(sampleQuestions);

Explanation:

  • Dynamically Creating Elements: This function goes through each question in the provided quiz, creates the necessary HTML elements dynamically, and appends them to the specified div in our HTML file. We’re using the createElement and appendChild methods to construct our quiz interface programmatically.
  • Classifying Question Types: Utilizing the 'options' in question type guard, we are discerning whether a question is of type MultipleChoiceQuestion or TrueFalseQuestion, allowing us to render the corresponding options correctly.
  • Setting Attributes and Classes: For each dynamically created element, we are setting attributes like type, name, id, and className to ensure our elements are displayed and function as intended, corresponding to our predefined styles and structure.
  • Dynamic DOM Elements: Since our DOM elements are created dynamically, their IDs are also generated dynamically. For example, the expression q${index + 1}${char} is used to create unique and predictable IDs for each MCQ option. The appended character is generated using const char = String.fromCharCode(97 + i). This trick allows us to generate letters in sequence ('a', 'b', 'c', ...). The ASCII code for 'a' is 97, so adding the index i gives us the desired letter.

You might wonder why we didn’t simply set quizDiv.innerHTML = '' to clear all the child nodes, which is seemingly more straightforward. While setting innerHTML to an empty string is indeed a quick method to clear all child nodes, it has its caveats:

  1. Performance Concerns: It can be less efficient due to string parsing and HTML content application, especially with large DOM structures.
  2. Memory Management: Potential memory leaks can occur if there are event listeners attached to the child nodes, as this method might not always release the associated memory.
  3. Total Clearance: It's a destructive method, removing all content, including text nodes and non-element nodes, which may not be desired in all cases.

By using a loop to remove each child individually, we ensure better memory management and provide an opportunity for any cleanup processes to run, mitigating the risk of memory leaks and preserving non-element nodes if needed.

This approach exemplifies how a deeper understanding of JavaScript and the Document Object Model (DOM) intertwines with TypeScript to create more efficient, reliable, and maintainable applications.

Step 5: Evaluate User’s Submission

Once the user answers the quiz questions and hits the "Submit" button, we want to evaluate their answers against the correct answers and provide a score. In order to achieve this, we create an event listener for the button, which will handle the scoring logic.

function createSubmitHandler(quiz: Quiz) {
  return function handleSubmit(event: Event) {
    event.preventDefault();
    const questions: QuizQuestion[] = quiz.questions;
    let score = 0;

    questions.forEach((question: QuizQuestion) => {
      if ("options" in question) {
        // Multiple Choice Questions
        question.options.forEach((option: string, index: number) => {
          const char = String.fromCharCode(97 + index); // 97 is ASCII for 'a'
          const id = `q${question.id}${char}`;
          const elm = document.getElementById(id) as HTMLInputElement;

          if (elm.checked) {
            if (option === question.answer) {
              score++;
            }
            elm.checked = false;
          }
        });
      } else {
        // True/False Choice Questions
        [true, false].forEach((option: boolean) => {
          const id = `q${question.id}${option ? "t" : "f"}`;
          const elm = document.getElementById(id) as HTMLInputElement;

          if (elm.checked) {
            if (option === question.answer) {
              score++;
            }
            elm.checked = false;
          }
        });
      }
    });

    alert(`Your score is ${score} out of ${quiz.questions.length}`);
  };
}

renderQuiz(sampleQuestions);

const submitButton = document.getElementById("submit-btn");
if (submitButton) {
  submitButton.addEventListener("click", createSubmitHandler(sampleQuestions));
}

Explanation:

  1. Factory Method (createSubmitHandler): Instead of directly creating an event listener, we're creating a function that returns an event listener. This allows us to parameterize the event listener with specific data – in this case, the quiz object. This makes our function more reusable and modular.
  2. Handling Different Question Types: As you might recall, our quiz can contain two different types of questions: multiple choice and true/false. Hence, within our submit handler, we first determine the type of question by checking for the presence of the options property.
  3. Dynamically Accessing DOM Elements: To determine which option a user selected, we use the document.getElementById method. Since our DOM elements are dynamically created, their IDs are also dynamically generated. For example, the expression q${index + 1}${String.fromCharCode(97 + i)} is used to generate unique and predictable IDs for each multiple-choice question (MCQ) option. The portion String.fromCharCode(97 + i) is a trick to generate letters in sequence ('a', 'b', 'c', ...). The ASCII code for 'a' is 97, so adding i (the index of the option) gives us the desired letter.
  4. Scoring: The score is incremented based on the user's selected option matching the correct answer. For multiple choice questions, the selected option's text should match the answer property. For true/false questions, the boolean value of the selected option should match.
  5. Alerting the Score: Once all questions are checked, an alert displays the user’s score. This gives immediate feedback on their performance.

With this implementation, we've essentially allowed our application to grade any given quiz dynamically based on user input.

Step 6: Add more Questions

As the final step, update the sampleQuestions and add more questions.

const sampleQuestions: Quiz = {
  title: "TypeScript Quiz",
  questions: [
    {
      id: 1,
      text: "TypeScript is a superset of JavaScript.",
      answer: true,
    } as TrueFalseQuestion,
    {
      id: 2,
      text: "TypeScript cannot be run in a web browser.",
      answer: true,
    } as TrueFalseQuestion,
    {
      id: 3,
      text: "TypeScript was developed by Microsoft.",
      answer: true,
    } as TrueFalseQuestion,
    {
      id: 4,
      text: "Which one can be used to define an enumeration?",
      options: [
        "enum Keyword",
        "enumeration Type",
        "enum Type",
        "enumerable Keyword",
      ],
      answer: "enum Keyword",
    } as MultipleChoiceQuestion,
    {
      id: 5,
      text: "Which one is NOT a TypeScript data type?",
      options: ["string", "number", "boolean", "character"],
      answer: "character",
    } as MultipleChoiceQuestion,
  ],
};