1. はじめに
JavaScriptの学習の一環として、ドットインストールのレッスンにある三択クイズアプリを作ってみました。この記事では、DOM操作・配列・forEach()を組み合わせてクイズを動的に表示する実装の流れを学習メモとして整理しています。
今回作ったのは「選択肢をクリックすると正解・不正解が表示される」シンプルなクイズアプリです。
2. HTMLとCSSの準備
まず完成形のマークアップとスタイリングを先に用意しました。この章では実際に書いたコードをまとめておきます。
2.1 HTML
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>三択クイズ</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<main>
<h1>三択クイズ</h1>
</main>
<script src="main.js"></script>
</body>
</html>
2.2 CSS
@charset "utf-8";
main {
width: 320px;
margin: 32px auto 0;
}
h1 {
font-size: 20px;
text-align: center;
border-bottom: 1px solid #ccc;
padding-bottom: 8px;
}
h2 {
font-size: 16px;
}
li {
cursor: pointer;
}
.correct {
color: green;
}
.correct::after {
content: " - 正解!";
}
.wrong {
color: red;
}
.wrong::after {
content: " - 不正解...";
}
3. JavaScriptの実装
この章では、クイズデータの定義・DOM操作による要素追加・クリックイベントの処理・forEach()を使ったループ処理の流れをまとめています。
3.1 クイズデータの準備
クイズの問題文・選択肢・正解番号を配列で定義しています。
"use strict";
{
const quizzes = [
// [問題文, 選択肢A, 選択肢B, 選択肢C, 正解の番号(0始まり)]
["1の正解は?", "選択肢A", "選択肢B", "選択肢C", 0],
["2の正解は?", "選択肢A", "選択肢B", "選択肢C", 1],
["3の正解は?", "選択肢A", "選択肢B", "選択肢C", 2],
];
}
正解番号は0始まりのインデックスで管理しているようです。0なら選択肢A、1なら選択肢B、2なら選択肢Cが正解という意味みたいです。
3.2 DOM操作でセクションを生成する
render()という関数を作り、クイズデータを受け取ってDOM要素を組み立てる構成にしているようです。
const render = (quiz) => {
const main = document.querySelector("main");
const section = document.createElement("section");
main.appendChild(section);
const h2 = document.createElement("h2");
h2.textContent = quiz[0];
section.appendChild(h2);
const ul = document.createElement("ul");
section.appendChild(ul);
const li0 = document.createElement("li");
li0.textContent = quiz[1];
ul.appendChild(li0);
const li1 = document.createElement("li");
li1.textContent = quiz[2];
ul.appendChild(li1);
const li2 = document.createElement("li");
li2.textContent = quiz[3];
ul.appendChild(li2);
};
appendChild()の順番はとても重要みたいです。呼び出す順番がそのまま要素が追加される順番になるので、h2とulの順番を逆にしてしまうとブラウザの表示も逆になってしまいます。
3.3 クリックイベントで正解・不正解を判定する
各li要素に対してクリックイベントを追加し、正解番号と照合してクラスを付け替えているようです。
li0.addEventListener("click", () => {
if (quiz[4] === 0) {
li0.classList.add("correct");
} else {
li0.classList.add("wrong");
}
});
li1.addEventListener("click", () => {
if (quiz[4] === 1) {
li1.classList.add("correct");
} else {
li1.classList.add("wrong");
}
});
li2.addEventListener("click", () => {
if (quiz[4] === 2) {
li2.classList.add("correct");
} else {
li2.classList.add("wrong");
}
});
3.4 forEachでループ処理にまとめる
最初はrender()を3回手で呼び出していたのですが、問題が増えるたびに追加しなければいけないので面倒でした。forEach()を使うとすっきりまとめられるみたいです。
quizzes.forEach((quiz) => {
render(quiz);
});
forEach()は配列の要素を1つずつ取り出して処理してくれるメソッドのようです。引数に渡した関数が各要素に対して順番に実行されるみたいです。配列と組み合わせてよく使われるテクニックとのことで、覚えておきたいと思いました。
4. リファクタリング前JavaScriptコード全体
この章では、ここまでの実装をひとつにまとめたコードを掲載しています。
"use strict";
{
const quizzes = [
["1の正解は?", "選択肢A", "選択肢B", "選択肢C", 0],
["2の正解は?", "選択肢A", "選択肢B", "選択肢C", 1],
["3の正解は?", "選択肢A", "選択肢B", "選択肢C", 2],
];
const render = (quiz) => {
const main = document.querySelector("main");
const section = document.createElement("section");
main.appendChild(section);
const h2 = document.createElement("h2");
h2.textContent = quiz[0];
section.appendChild(h2);
const ul = document.createElement("ul");
section.appendChild(ul);
const li0 = document.createElement("li");
li0.textContent = quiz[1];
ul.appendChild(li0);
const li1 = document.createElement("li");
li1.textContent = quiz[2];
ul.appendChild(li1);
const li2 = document.createElement("li");
li2.textContent = quiz[3];
ul.appendChild(li2);
li0.addEventListener("click", () => {
if (quiz[4] === 0) {
li0.classList.add("correct");
} else {
li0.classList.add("wrong");
}
});
li1.addEventListener("click", () => {
if (quiz[4] === 1) {
li1.classList.add("correct");
} else {
li1.classList.add("wrong");
}
});
li2.addEventListener("click", () => {
if (quiz[4] === 2) {
li2.classList.add("correct");
} else {
li2.classList.add("wrong");
}
});
};
quizzes.forEach((quiz) => {
render(quiz);
});
}
5. 自分なりにfor文でリファクタリングしてみた
レッスンのまとめで「li要素の生成もループでまとめられそう」という話があったので、自分なりにfor文を使ってリファクタリングしてみました。
5.1 変更点の整理
元のコードではli0・li1・li2とそれぞれのイベントリスナーを個別に書いていましたが、for文にまとめることで同じ処理の繰り返しをなくせるのではと考えてみました。
5.2 リファクタリング後のコード
"use strict";
{
// 問題文, 選択肢, 選択肢, 選択肢, 正解(0, 1, 2)
const quizzes = [
["1の正解は?", "選択肢 A", "選択肢 B", "選択肢 C", 0],
["2の正解は?", "選択肢 A", "選択肢 B", "選択肢 C", 1],
["3の正解は?", "選択肢 A", "選択肢 B", "選択肢 C", 2],
];
function render(quiz) {
const main = document.querySelector("main");
const section = document.createElement("section");
main.appendChild(section);
const h2 = document.createElement("h2");
h2.textContent = quiz[0];
section.appendChild(h2);
const ul = document.createElement("ul");
section.appendChild(ul);
for (let i = 0; i < 3; i++) {
const li = document.createElement("li");
li.textContent = quiz[i + 1];
ul.appendChild(li);
li.addEventListener("click", () => {
if (quiz[4] === i) {
li.classList.add("correct");
} else {
li.classList.add("wrong");
}
});
}
}
quizzes.forEach((quiz) => {
render(quiz);
});
}
5.3 やってみて気づいたこと
iが0・1・2と変化するので、quiz[i + 1]で選択肢A・B・Cを順番に取り出せる構造にしてみました。
クリックイベントの正解判定もquiz[4] === iと書くことで、個別に=== 0・=== 1・=== 2と書く必要がなくなってすっきりしたと思います。
for文のブロック内でlet iを使っているのがポイントみたいです。varだとイベントリスナーの中で参照するiの値がループ終了後の値(3)に固定されてしまうとのことで、letを使うことでクリック時に正しいiの値が参照できるようです。
元のコードでli0・li1・li2と書いていた部分をfor文に置き換えると、変数名で「どの選択肢か」を区別できなくなります。その分、インデックスiの意味をしっかり意識しながら読む必要があるみたいです。
まとめ
今回の学習で理解できたと思うことをまとめておきます。
createElement()とappendChild()を組み合わせることで、配列データからDOM要素を動的に生成できるみたいです。appendChild()の呼び出し順が表示順に直結する点は最初に気づきにくくてハマりました。
また、forEach()を使うことでループ処理をすっきり書けるようになり、問題数が変わってもコードを変更しなくて済む構造にできたのは大きな気づきでした。
さらに自分なりにfor文でリファクタリングを試してみることで、letとvarのスコープの違いや、インデックスiをうまく活用する書き方について理解が深まったと思います。
