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
asQuiz
, you are specifying thatsampleQuestions
must follow the structure defined by theQuiz
interface. This provides type safety, ensuring that the object's shape is as expected. - Type Assertions (
as TrueFalseQuestion
): Theas TrueFalseQuestion
is a type assertion, explicitly instructing TypeScript to treat the object as aTrueFalseQuestion
. 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 ourquestion
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 specifieddiv
in our HTML file. We’re using thecreateElement
andappendChild
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 typeMultipleChoiceQuestion
orTrueFalseQuestion
, 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
, andclassName
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 usingconst 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 indexi
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:
- Performance Concerns: It can be less efficient due to string parsing and HTML content application, especially with large DOM structures.
- 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.
- 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:
- 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, thequiz
object. This makes our function more reusable and modular. - 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. - 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 expressionq${index + 1}${String.fromCharCode(97 + i)}
is used to generate unique and predictable IDs for each multiple-choice question (MCQ) option. The portionString.fromCharCode(97 + i)
is a trick to generate letters in sequence ('a', 'b', 'c', ...). The ASCII code for 'a' is 97, so addingi
(the index of the option) gives us the desired letter. - 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. - 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,
],
};