Web開発を学んでいる人から「どうすればJavaScriptでクイズを作れますか」という質問を頻繁に受けます。新しいことを楽しみながら学んでもらうのには、クイズが適しているためです。
JavaScriptを使ったクイズのコーディングは、JavaScriptの良い勉強になります。イベントの処理方法、ユーザーインプットの取り扱い、DOM操作、フィードバックの返し方、スコアのトラック方法(たとえばクライアントサイドに保存)などを学べます。基本的なクイズができたら、ページ区切りを追加して機能を充実します。追加できる機能はこの記事の最後に取り上げます。
この記事では、JavaScriptでクイズを作るステップを説明します。Webサイトに合わせて適宜改変してください。完成形はクイズのデモで確認できます。
開始前の確認事項
始める前に前提条件をお伝えします。
- この記事のクイズはフロントエンドで完結する。つまりブラウザー内にデータが存在するので、コードを見れば答えが分かる。重要なクイズはバックエンドにデータを格納するがこの記事では解説しない
- この記事のコードはES2015の文法に従うためInternet Explorerのすべてのバージョンがサポートしていない。ただしMicrosoft Edgeを含む最近のブラウザーでは実行できる
- 古いブラウザーで使うなら、IE8までをサポートするJavaScriptクイズの記事が参考になる
- 詳しい説明をいれているものの、HTMLやCSS、JavaScriptに関する基本的な知識があることを前提としている
JavaScriptクイズの基本構造
質問と回答の一覧をJavaScriptコード内に記述します。これで自動的にクイズを生成できます。同じHTMLタグを何回も書かなくてよくなり、さらに質問の追加や削除が簡単にできます。
JavaScriptクイズの基本構造です。
- <div>にクイズを格納
- <button>でクイズを送信
- <div>に結果を表示
具体的な記述は以下の通りです。
<div id="quiz"></div>
<button id="submit">Submit Quiz</button>
<div id="results"></div>
これらのHTMLタグを選択し、参照を変数に格納します。
const quizContainer = document.getElementById('quiz');
const resultsContainer = document.getElementById('results');
const submitButton = document.getElementById('submit');
次にクイズを生成する機能、結果を表示する機能、それを統合する機能を作ります。中身はあとで作るので、ここでは関数名を記述します。
function buildQuiz(){}
function showResults(){}
// display quiz right away
buildQuiz();
// on submit, show results
submitButton.addEventListener('click', showResults);
これは、クイズを生成して結果を表示する関数です。buildQuiz関数はただちに実行し、showResults関数はユーザーがSubmitボタンを押したときに実行します。
クイズの質問を表示する
クイズの質問を表示させます。ここでは個々の質問をオブジェクトリテラルで表現し、その配列がクイズに含める質問全体です。配列を使うことで1問ずつ質問にアクセスするのが簡単になります。
const myQuestions = [
{
question: "Who is the strongest?",
answers: {
a: "Superman",
b: "The Terminator",
c: "Waluigi, obviously"
},
correctAnswer: "c"
},
{
question: "What is the best site ever created?",
answers: {
a: "SitePoint",
b: "Simple Steps Code",
c: "Trick question; they're both the best"
},
correctAnswer: "c"
},
{
question: "Where is Waldo really?",
answers: {
a: "Antarctica",
b: "Exploring the Pacific Ocean",
c: "Sitting in a tree",
d: "Minding his own business, so stop asking"
},
correctAnswer: "d"
}
];
必要な数だけの質問と解答を書きます。
質問の一覧ができました。次のJavaScriptコードで質問を表示します。
function buildQuiz(){
// we'll need a place to store the HTML output
const output = [];
// for each question...
myQuestions.forEach(
(currentQuestion, questionNumber) => {
// we'll want to store the list of answer choices
const answers = [];
// and for each available answer...
for(letter in currentQuestion.answers){
// ...add an HTML radio button
answers.push(
`<label>
<input type="radio" name="question${questionNumber}" value="${letter}">
${letter} :
${currentQuestion.answers[letter]}
</label>`
);
}
// add this question and its answers to the output
output.push(
`<div class="question"> ${currentQuestion.question} </div>
<div class="answers"> ${answers.join('')} </div>`
);
}
);
// finally combine our output list into one string of HTML and put it on the page
quizContainer.innerHTML = output.join('');
}
コードについて1行ずつ説明します。
まずはHTML出力を格納する変数outputを用意します。この変数に質問と解答の選択肢を入力します。
次に、それぞれの質問のHTMLを生成するために、forEachループで順番に質問を処理します。
myQuestions.forEach( (currentQuestion, questionNumber) => {
// here goes the code we want to run for each question
});
ここではアロー関数で各質問に対する操作を簡潔に記述しています。forEachループなので、現在の値とインデックス(配列中の現在のアイテムの位置を示す数字)と配列をパラメーターとして受け取ります。現在の値とインデックスが必要で、それぞれcurrentQuestionとquestionNumberと名付けています。
続いてループの中のコードを説明します。
// we'll want to store the list of answer choices
const answers = [];
// and for each available answer...
for(letter in currentQuestion.answers){
// ...add an html radio button
answers.push(
`<label>
<input type="radio" name="question${questionNumber}" value="${letter}">
${letter} :
${currentQuestion.answers[letter]}
</label>`
);
}
// add this question and its answers to the output
output.push(
`<div class="question"> ${currentQuestion.question} </div>
<div class="answers"> ${answers.join('')} </div>`
);
それぞれの質問に対してHTMLを生成します。まずは解答の選択肢を格納する配列を用意します。
続いて解答の選択肢ごとにループを使ってHTMLのラジオボタンを生成します。ここではテンプレートリテラルを使っています。ただの文字列ではなく、次の機能があります。
- 複数行に対応可能
- テンプレートリテラルはバッククォートを使っているので、引用符内の引用符をエスケープしなくてよい
- 文字列を挿入できるので、${code_goes_here}のようにJavaScriptコードをそのまま文字列に埋め込める
解答のラジオボタンを作り終えたら、質問と解答のHTMLを出力に追加します。
テンプレートリテラルと埋め込み文字列を使って、質問と解答のdivを生成します。解答の選択肢はjoin関数で1つの文字列に結合してから、answers divに挿入します。
各質問のHTMLができたら、すべてを結合してページに表示します。
quizContainer.innerHTML = output.join('');
これでbuildQuiz関数が完成しました。
クイズの結果を表示する
ループで順に解答をチェックして結果を表示するshowResults関数を作ります。
まずは関数を示します。あとで詳しく説明します。
function showResults(){
// gather answer containers from our quiz
const answerContainers = quizContainer.querySelectorAll('.answers');
// keep track of user's answers
let numCorrect = 0;
// for each question...
myQuestions.forEach( (currentQuestion, questionNumber) => {
// find selected answer
const answerContainer = answerContainers[questionNumber];
const selector = 'input[name=question'+questionNumber+']:checked';
const userAnswer = (answerContainer.querySelector(selector) || {}).value;
// if answer is correct
if(userAnswer===currentQuestion.correctAnswer){
// add to the number of correct answers
numCorrect++;
// color the answers green
answerContainers[questionNumber].style.color = 'lightgreen';
}
// if answer is wrong or blank
else{
// color the answers red
answerContainers[questionNumber].style.color = 'red';
}
});
// show number of correct answers out of total
resultsContainer.innerHTML = numCorrect + ' out of ' + myQuestions.length;
}
クイズのHTMLからすべての解答コンテナを選択します。正答数と現在の質問に対してユーザーが選択した解答を格納する変数を用意します。
// gather answer containers from our quiz
const answerContainers = quizContainer.querySelectorAll('.answers');
// keep track of user's answers
let numCorrect = 0;
質問の解答をループを使って順番にチェックします。
// for each question...
myQuestions.forEach( (currentQuestion, questionNumber) => {
// find selected answer
const answerContainer = answerContainers[questionNumber];
const selector = `input[name=question${questionNumber}]:checked`;
const userAnswer = (answerContainer.querySelector(selector) || {}).value;
// if answer is correct
if(userAnswer===currentQuestion.correctAnswer){
// add to the number of correct answers
numCorrect++;
// color the answers green
answerContainers[questionNumber].style.color = 'lightgreen';
}
// if answer is wrong or blank
else{
// color the answers red
answerContainers[questionNumber].style.color = 'red';
}
});
このコードの要点をまとめます。
- ユーザーが選択した解答をHTMLから取得
- 正答の場合の処理
- 誤答の場合の処理
ユーザーが選択した解答をHTMLから取得する方法を詳しく説明します。
// find selected answer
const answerContainer = answerContainers[questionNumber];
const selector = `input[name=question${questionNumber}]:checked`;
const userAnswer = (answerContainer.querySelector(selector) || {}).value;
現在の質問に対する解答コンテナを取得し、次の行で、チェックされているラジオボタンを取得するCSSセレクターを定義します。
先ほど定義したanswerContainerのうちCSSセレクターに合致するものをJavaScriptのquerySelectorで取得します。どの解答のラジオボタンが選択されているのかを調べるのです。
最後に.valueでその解答の値を取得します。
不完全なユーザーインプットへの対応
無解答なら、存在しない値は取得できないので、.valueはエラーを起こします。そこでorを意味する||と空のオブジェクトを意味する{}を追加します。できあがったコードは次の処理をします。
- ユーザーが選択した解答のエレメントへの参照を取得する。存在しなければ、空のオブジェクトを使う
- 上記の値を取得する
最終的に取得する値はユーザーの解答かundefinedです。これでユーザーが無解答でもクイズのクラッシュを防げます。
解答を評価して結果を表示
解答をチェックするループにある次のコードは、正答と誤答を処理しています。
// if answer is correct
if(userAnswer===currentQuestion.correctAnswer){
// add to the number of correct answers
numCorrect++;
// color the answers green
answerContainers[questionNumber].style.color = 'lightgreen';
}
// if answer is wrong or blank
else{
// color the answers red
answerContainers[questionNumber].style.color = 'red';
}
ユーザーの解答が正答と一致した場合には、正答数に1を足し、オプションで選択肢を緑色にします。誤答か無回答の場合には、こちらもオプションで選択肢を赤色にします。
解答をチェックするループが終わったら、ユーザーが正答した質問の数を表示します。
// show number of correct answers out of total
resultsContainer.innerHTML = `${numCorrect} out of ${myQuestions.length}`;
これでJavaScriptのクイズができました。
必須ではありませんが、クイズのコード全体をIIFE(immediately invoked function expression)に入れることも一案です。IIFEは定義した直後に実行される関数で、変数をグローバルスコープからローカルスコープにできるので、クイズのコードとほかのスクリプトの干渉を避けられます。
(function(){
// put the rest of your code here
})();
準備完了です。これを基に質問や解答、クイズのスタイルなど、思いのままに修正を加えてください。
現時点でクイズは次のように表示されます(少しだけスタイルを修正しています)。
ページ区切りを追加
クイズができたので、これから発展的な機能を追加します。たとえば、質問を1問ずつ表示できるようにします。
そのためには以下が必要です。
- 質問を表示する機能と非表示にする機能
- クイズのナビゲーションボタン
既存のコードを修正します。まずはHTMLです。
<div class="quiz-container">
<div id="quiz"></div>
</div>
<button id="previous">Previous Question</button>
<button id="next">Next Question</button>
<button id="submit">Submit Quiz</button>
<div id="results"></div>
HTMLタグはほとんど変わっていませんが、ナビゲーションボタンとクイズのコンテナを追加しました。クイズのコンテナに質問を入れて、表示・非表示を切り替えます。
buildQuiz関数にslideクラスを持つ<div>エレメントを追加して、作成済みの質問と解答のコンテナを配置します。
output.push(
`<div class="slide">
<div class="question"> ${currentQuestion.question} </div>
<div class="answers"> ${answers.join("")} </div>
</div>`
);
次にCSSを使って、スライドをレイヤーで重ねて表示します。今回はz-indexとopacityで、スライドをフェードイン、アウトさせます。
.slide{
position: absolute;
left: 0px;
top: 0px;
width: 100%;
z-index: 1;
opacity: 0;
transition: opacity 0.5s;
}
.active-slide{
opacity: 1;
z-index: 2;
}
.quiz-container{
position: relative;
height: 200px;
margin-top: 40px;
}
続いてページ区切りを追加します。
ナビゲーションボタンへの参照と表示中のスライドを格納する変数を追加します。
// pagination
const previousButton = document.getElementById("previous");
const nextButton = document.getElementById("next");
const slides = document.querySelectorAll(".slide");
let currentSlide = 0;
スライドを表示する関数です。
function showSlide(n) {
slides[currentSlide].classList.remove('active-slide');
slides[n].classList.add('active-slide');
currentSlide = n;
if(currentSlide===0){
previousButton.style.display = 'none';
}
else{
previousButton.style.display = 'inline-block';
}
if(currentSlide===slides.length-1){
nextButton.style.display = 'none';
submitButton.style.display = 'inline-block';
}
else{
nextButton.style.display = 'inline-block';
submitButton.style.display = 'none';
}
}
showSlide(0);
1~3行目で以下を実施しています。
- 表示中のスライドからactive-slideクラスを削除して、非表示にする
- 次のスライドにactive-slideクラスを追加して、表示する
- 表示中のスライド番号を更新する
それ以降の行では以下のロジックを実装しています。
- 始めのスライドなら、Previous Slideボタンを非表示にする。それ以外なら表示する
- 最後のスライドなら、Next Slideボタンを非表示にして、Submitボタンを表示する。それ以外なら、Next Slideボタンを表示して、Submitボタンを非表示にする
この関数を定義した直後に、showSlide(0)を呼び出して始めのスライドを表示します。
続いてナビゲーションボタンを作動させる関数を作ります。
function showNextSlide() {
showSlide(currentSlide + 1);
}
function showPreviousSlide() {
showSlide(currentSlide - 1);
}
previousButton.addEventListener("click", showPreviousSlide);
nextButton.addEventListener("click", showNextSlide);
showSlide関数で、ナビゲーションボタンを使って前後のスライドへ移動できるようにしています。
これでナビゲーションが完成です。
カスタマイズ
作成したJavaScriptクイズをベースに、自由にカスタマイズしてください。
たとえば、正答と誤答へのレスポンスを変えてください。ほかにも、このクイズのコンセプトをほかのことに転用できるかもしれません。
カスタマイズのヒントを挙げます。
- クイズのスタイルを変更する
- プログレスバーを追加する
- 送信前に解答を確認できるようにする
- 送信した解答を一覧で見られるようにする
- 任意の質問に移動できるようにナビゲーションを改良する
- 結果に応じたメッセージを表示する。たとえば10問中8問以上正解だったら、クイズ名人の称号を与える
- 結果をSNSで共有するボタンを追加する
- localStorageを使って最高点を保存できるようにする
- カウントダウンタイマーを追加して制限時間を設ける
- この記事のコンセプトを利用して、プロジェクト費用計算機や性格診断機を作成する
(原文:How to Make a Simple JavaScript Quiz)
[翻訳:内藤 夏樹/編集:Livit]