JavaScriptでクイズ系のコンテンツを作成してみる

JavaScriptを使って、簡単なクイズコンテンツを作ってみます。

サンプルコード

まずは各画面のコーディングを行います。
今回は以下の4画面を使用します。

  • 扉画面
  • 問題画面
  • 回答・解説画面
  • 結果画面

まずは扉画面です。
jsで処理を行う要素にはjs-XXX の形でclassをつけています。

<div class="p-quiz js-quiz-content">
	<h1 class="p-quiz-ttl">クイズコンテンツサンプル</h1>
	<div class="p-quiz-next">
		<button class="c-btn js-quiz-start">開始</button>
	</div>
</div>

扉画面のデモページ

問題画面です。

<div class="p-quiz js-quiz-content">
	<h1 class="p-quiz-ttl">2019年8月8日にデビューしたにじさんじメンバーのユニット名はなに?</h1>
	<ol class="p-quiz-choices">
		<li class="p-quiz-choices__item">
			<button class="c-btn js-quiz-choice" data-quiz_choice="1">SMC組</button>
		</li>
		<li class="p-quiz-choices__item">
			<button class="c-btn js-quiz-choice" data-quiz_choice="2">ぶるーず</button>
		</li>
		<li class="p-quiz-choices__item">
			<button class="c-btn js-quiz-choice" data-quiz_choice="3">ぽさんけ</button>
		</li>
		<li class="p-quiz-choices__item">
			<button class="c-btn js-quiz-choice" data-quiz_choice="4">チューリップ組</button>
		</li>
	</ol>
</div>

選択肢の何番目を選択したかはdata-quiz_choiceを使って管理します。
問題画面のデモページ

回答・解説画面です。

<div class="p-quiz js-quiz-content">
	<h1 class="p-quiz-ttl">2019年8月8日にデビューしたにじさんじメンバーのユニット名はなに?</h1>
	<p class="p-quiz-result">正解</p>
	<p class="p-quiz-commentary">ぽさんけは天宮こころ、エリー・コニファー、ラトナ・プティの3人で構成されるユニット。</p>
	<div class="p-quiz-next">
		<button class="c-btn js-quiz-next">次の問題</button>
	</div>
</div>

回答・解説画面のデモページ

結果画面です。

<div class="p-quiz js-quiz-content">
	<h1 class="p-quiz-ttl">結果は5問中3問正解でした</h1>
	<p class="p-quiz-commentary">まあまあです。</p>
	<div class="p-quiz-next">
		<button class="c-btn js-quiz-top">トップに戻る</button>
	</div>
</div>

結果画面のデモページ

各画面にCSSを当てて、各画面の準備は完了です。

.c-btn {
	-webkit-appearance: none;
	appearance: none;
	display: inline-flex;
	align-items: center;
	justify-content: center;
	width: 100%;
	height: 40px;
	padding: 0;
	border: none;
	outline: none;
	color: white;
	font-size: 16px;
	text-decoration: none;
	background: gray;
	cursor: pointer;
}
.p-quiz {
	width: 600px;
	margin: 50px auto;
	text-align: center;
}
.p-quiz-ttl {
	margin: 0 0 20px;
	font-size: 24px;
	font-weight: normal;
}
.p-quiz-next {
	width: 300px;
	margin: auto;
}
.p-quiz-choices {
	display: inline-flex;
	flex-wrap: wrap;
	width: 100%;
	margin: 0;
	padding-left: 0;
}
.p-quiz-choices__item {
	width: calc(50% - 10px);
	list-style: none;
	margin: 5px;
}

次にJavaScriptでの処理を追加してみます。
先ほど作成した各画面をjsから出力する構造にするため、HTML上は以下の要素のみにします。

<div class="p-quiz js-quiz-content"></div>

クイズの内容を管理するquiz.jsonを作成します。
quizの値がクイズの内容、rankの値が結果画面で使用する内容になります。

{
	"quiz": [
		{
			"q": "2019年8月8日にデビューしたにじさんじメンバーのユニット名はなに?",
			"a": [
				"SMC組",
				"ぶるーず",
				"ぽさんけ",
				"チューリップ組"
			],
			"correct": 3,
			"commentary": "ぽさんけは天宮こころ、エリー・コニファー、ラトナ・プティの3人で構成されるユニット。"
		},

~略~

		{
			"q": "レイン・パターソンの誕生日はいつ?",
			"a": [
				"10月27日",
				"9月6日",
				"2月21日",
				"1月13日"
			],
			"correct": 4,
			"commentary": "10月27日はアクシア・クローネ、9月6日はローレン・イロアス、2月21日はオリバー・エバンスの誕生日。"
		}
	],
	"rank": [
		{
			"count": 5,
			"comment": "すごいです。"
		}, {
			"count": 3,
			"comment": "まあまあです。"
		}, {
			"count": 1,
			"comment": "そんなにです。"
		}, {
			"count": 0,
			"comment": "だめです。"
		}
	]
}

JavaScriptでquiz.jsonを読み込んで、画面を出力したりイベントを設定する処理を追加します。

let quizData = {};
let currentQuizNo = 0;
let correctCount = 0;

// 問題データの取得
get_quiz_data();
// トップ画面の生成
generate_top_content();
// 問題開始のイベント設定
register_start_event();

/**
 * 問題のデータを取得する
 * 
 */
function get_quiz_data() {
	let xhr = new XMLHttpRequest();
	xhr.onload = function() {
		quizData = xhr.response;
	}
	xhr.open('GET', 'quiz.json');
	xhr.responseType = "json";
	xhr.send();
}

/**
 * 問題開始のイベントを設定する
 * 
 */
function register_start_event() {
	document.querySelector('.js-quiz-start').addEventListener('click', function() {
		// 問題画面の生成
		generate_quiz_content();
		// 問題の選択肢を選択したときのイベント設定
		register_choice_event();
	}, false);
}

/**
 * 問題の選択肢を選択したときのイベントを設定する
 * 
 */
function register_choice_event() {
	for (var i = 0; i < document.querySelectorAll('.js-quiz-choice').length; i++) {
		document.querySelectorAll('.js-quiz-choice')[i].addEventListener('click', function(e) {
			// 回答・解説画面の生成
			generate_answer_content(parseFloat(this.getAttribute('data-quiz_choice')));
			// 未回答の問題がある場合
			if(currentQuizNo + 1 < quizData['quiz'].length) {
				// 次の問題へ遷移するときのイベント設定
				register_nextquiz_event();
			// 全て回答済の場合
			} else {
				// 結果画面へ遷移するときのイベント設定
				register_result_event();
			}
		}, false);
	}
}

/**
 * 次の問題へ遷移するときのイベントを設定する
 * 
 */
function register_nextquiz_event() {
	document.querySelector('.js-quiz-next').addEventListener('click', function() {
		currentQuizNo++;
		// 問題画面の生成
		generate_quiz_content();
		// 問題の選択肢を選択したときのイベント設定
		register_choice_event();
	}, false);
}

/**
 * 結果画面へ遷移するときのイベントを設定する
 * 
 */
function register_result_event() {
	document.querySelector('.js-quiz-result').addEventListener('click', function() {
		// 結果画面の生成
		generate_result_content();
		// トップへ遷移するときのイベント設定
		register_top_event();
	}, false);
}

/**
 * トップへ遷移するときのイベントを設定する
 * 
 */
function register_top_event() {
	document.querySelector('.js-quiz-top').addEventListener('click', function() {
		// 値のリセット
		currentQuizNo = 0;
		correctCount = 0;
		// トップ画面の生成
		generate_top_content();
		// 問題開始のイベント設定
		register_start_event();
	}, false);
}

/**
 * トップ画面を生成する
 * 
 */
// 
function generate_top_content() {
	var ins = '<h1 class="p-quiz-ttl">クイズコンテンツサンプル</h1>';
	ins += '<div class="p-quiz-next">';
		ins += '<button class="c-btn js-quiz-start">開始</button>';
	ins += '</div>';

	document.querySelector('.js-quiz-content').innerHTML = ins;
}

/**
 * 問題画面を生成する
 * 
 */
function generate_quiz_content() {
	var ins = '<h1 class="p-quiz-ttl">' + quizData['quiz'][currentQuizNo]['q'] + '</h1>';
	ins += '<ol class="p-quiz-choices">';
		for (var i = 0; i < quizData['quiz'][currentQuizNo]['a'].length; i++) {
			ins += '<li class="p-quiz-choices__item">';
				ins += '<button class="c-btn js-quiz-choice" data-quiz_choice="' + (i+1) + '">' + quizData['quiz'][currentQuizNo]['a'][i] + '</button>';
			ins += '</li>';
		}
	ins += '</ol>';

	document.querySelector('.js-quiz-content').innerHTML = ins;
}

/**
 * 回答・解説画面を生成する
 * @param {number} choice - 選択した回答番号
 */
function generate_answer_content(choice) {
	var ins = '<h1 class="p-quiz-ttl">' + quizData['quiz'][currentQuizNo]['q'] + '</h1>';
	// 正解の場合
	if(quizData['quiz'][currentQuizNo]['correct'] === choice) {
		ins += '<p class="p-quiz-result">正解</p>';
		correctCount++;
	// 不正解の場合
	} else {
		ins += '<p class="p-quiz-result">不正解</p>';
	}
	ins += '<p class="p-quiz-commentary">' + quizData['quiz'][currentQuizNo]['commentary'] + '</p>';
	// 未回答の問題がある場合
	if(currentQuizNo + 1 < quizData['quiz'].length) {
		ins += '<div class="p-quiz-next">';
			ins += '<button class="c-btn js-quiz-next">次の問題</button>';
		ins += '</div>';
	// 全て回答済の場合
	} else {
		ins += '<div class="p-quiz-next">';
			ins += '<button class="c-btn js-quiz-result">結果を見る</button>';
		ins += '</div>';
	}

	document.querySelector('.js-quiz-content').innerHTML = ins;
}

/**
 * 結果画面を生成する
 * 
 */
function generate_result_content() {
	var ins = '<h1 class="p-quiz-ttl">結果は' + (currentQuizNo+1) + '問中' + correctCount + '問正解でした</h1>';
	for (var i = 0; i < quizData['rank'].length; i++) {
		if(correctCount >= quizData['rank'][i]['count']) {
			ins += '<p class="p-quiz-commentary">' + quizData['rank'][i]['comment'] + '</p>';
			break;
		}
	}
	ins += '<div class="p-quiz-next">';
		ins += '<button class="c-btn js-quiz-top">トップに戻る</button>';
	ins += '</div>';

	document.querySelector('.js-quiz-content').innerHTML = ins;
}

基本的にはコメントで追記している通りで、12~24行目のget_quiz_data()でjsonデータを取得、26~102行目の関数で各イベント設定、104行目~182行目の関数で画面の生成・切り替えを行っています。
これで簡単な内容ですが、クイズのコンテンツを作成できました。
クイズコンテンツのデモページ

扉と問題・回答、結果でページを分ける

先ほどは同一URLで動的に画面を切り替えましたが、例えば結果画面をSNSでシェアさせる場合、結果に応じてOGP画像などを変えたいということがあります。
その場合は結果に応じてページが必要になるため、PHPなどを使ってページを作成するか、結果の数分のHTMLページを用意するなどの対応が必要になります。

今回はJavaScriptでコンテンツを作成しているため、扉画面と問題・回答画面、結果画面の3ページにわけて、さらに結果画面は結果の数分だけページを作成してみます。

まずは各HTMLを用意します。
扉ページは最初に作成したコーディングとほぼ同じで問題ないですが、開始ボタンだけaタグでリンクできるように変更しておきます。

<div class="p-quiz">
	<h1 class="p-quiz-ttl">クイズコンテンツサンプル</h1>
	<div class="p-quiz-next">
		<a href="quiz.html" class="c-btn">開始</a>
	</div>
</div>

合わせてjs処理も行わなくなるので、js-XXXの形のclassも除去しています。

問題・回答ページはコンテンツが動的に変更されるので、先ほどと同じく以下のようにしておきます。

<div class="p-quiz js-quiz-content"></div>

結果ページも扉ページと同じく、トップに戻るボタンをリンクに変更とjs-XXXのclassを除去します。

<div class="p-quiz">
	<h1 class="p-quiz-ttl">結果は5問中5問正解でした</h1>
	<p class="p-quiz-commentary">すごいです。</p>
	<div class="p-quiz-next">
		<a href="./" class="c-btn">トップに戻る</a>
	</div>
</div>

今回の場合結果は6パターンなので、result01.htmlresult02.htmlresult03.htmlresult04.htmlresult05.htmlresult06.htmlのように各画面作成しました。

次にquiz.jsonのrankの項目を変更します。

{
	"quiz": [

~略~

	],
	"rank": [
		{
			"count": 5,
			"page": "result01.html"
		}, {
			"count": 4,
			"page": "result02.html"
		}, {
			"count": 3,
			"page": "result03.html"
		}, {
			"count": 2,
			"page": "result04.html"
		}, {
			"count": 1,
			"page": "result05.html"
		}, {
			"count": 0,
			"page": "result06.html"
		}
	]
}

結果に応じて遷移先を変更できるように項目を追加しました。

最後に問題・回答ページで使用するJavaScriptの処理です。

let quizData = {};
let currentQuizNo = 0;
let correctCount = 0;

// 問題データの取得
get_quiz_data();

/**
 * 問題のデータを取得する
 * 
 */
function get_quiz_data() {
	let xhr = new XMLHttpRequest();
	xhr.onload = function() {
		quizData = xhr.response;
		// 問題画面の生成
		generate_quiz_content();
		// 問題の選択肢を選択したときのイベント設定
		register_choice_event();
	}
	xhr.open('GET', 'quiz.json');
	xhr.responseType = "json";
	xhr.send();
}

/**
 * 問題の選択肢を選択したときのイベントを設定する
 * 
 */
function register_choice_event() {
	for (var i = 0; i < document.querySelectorAll('.js-quiz-choice').length; i++) {
		document.querySelectorAll('.js-quiz-choice')[i].addEventListener('click', function(e) {
			// 回答・解説画面の生成
			generate_answer_content(parseFloat(this.getAttribute('data-quiz_choice')));
			// 未回答の問題がある場合
			if(currentQuizNo + 1 < quizData['quiz'].length) {
				// 次の問題へ遷移するときのイベント設定
				register_nextquiz_event();
			}
		}, false);
	}
}

/**
 * 次の問題へ遷移するときのイベントを設定する
 * 
 */
function register_nextquiz_event() {
	document.querySelector('.js-quiz-next').addEventListener('click', function() {
		currentQuizNo++;
		// 問題画面の生成
		generate_quiz_content();
		// 問題の選択肢を選択したときのイベント設定
		register_choice_event();
	}, false);
}

/**
 * 問題画面を生成する
 * 
 */
function generate_quiz_content() {
	var ins = '<h1 class="p-quiz-ttl">' + quizData['quiz'][currentQuizNo]['q'] + '</h1>';
	ins += '<ol class="p-quiz-choices">';
		for (var i = 0; i < quizData['quiz'][currentQuizNo]['a'].length; i++) {
			ins += '<li class="p-quiz-choices__item">';
				ins += '<button class="c-btn js-quiz-choice" data-quiz_choice="' + (i+1) + '">' + quizData['quiz'][currentQuizNo]['a'][i] + '</button>';
			ins += '</li>';
		}
	ins += '</ol>';

	document.querySelector('.js-quiz-content').innerHTML = ins;
}

/**
 * 回答・解説画面を生成する
 * @param {number} choice - 選択した回答番号
 */
function generate_answer_content(choice) {
	var ins = '<h1 class="p-quiz-ttl">' + quizData['quiz'][currentQuizNo]['q'] + '</h1>';
	// 正解の場合
	if(quizData['quiz'][currentQuizNo]['correct'] === choice) {
		ins += '<p class="p-quiz-result">正解</p>';
		correctCount++;
	// 不正解の場合
	} else {
		ins += '<p class="p-quiz-result">不正解</p>';
	}
	ins += '<p class="p-quiz-commentary">' + quizData['quiz'][currentQuizNo]['commentary'] + '</p>';
	// 未回答の問題がある場合
	if(currentQuizNo + 1 < quizData['quiz'].length) {
		ins += '<div class="p-quiz-next">';
			ins += '<button class="c-btn js-quiz-next">次の問題</button>';
		ins += '</div>';
	// 全て回答済の場合
	} else {
		ins += '<div class="p-quiz-next">';
		for (var i = 0; i < quizData['rank'].length; i++) {
			if(correctCount === quizData['rank'][i]['count']) {
				ins += '<a href="' + quizData['rank'][i]['page'] + '" class="c-btn">結果を見る</a>';
				break;
			}
		}
		ins += '</div>';
	}

	document.querySelector('.js-quiz-content').innerHTML = ins;
}

クイズコンテンツの画面を分けた場合のデモページ
基本的には前のコードの流用で、扉画面や結果画面で使用していた部分を削除しています。
合わせて、すべての問題に回答した後の98~103行目で正解数に応じて遷移先の調整を行っています。

このエントリーをはてなブックマークに追加

関連記事

コメントを残す

メールアドレスが公開されることはありません。
* が付いている欄は必須項目です

CAPTCHA


コメントが承認されるまで時間がかかります。

2022年5月
1234567
891011121314
15161718192021
22232425262728
293031