プログラミング初心者のための「単語帳風クイズラーニングアプリ」開発入門~第1部 学習データ管理編~

今回は、「単語帳風クイズラーニングアプリ」の作り方をご紹介していきたいと思います。

このアプリは「単語帳」のように、「自分で学びたい内容」を入力できて、気軽に学習を行うことができるため、

  • 英語学習
  • 学校のテスト勉強
  • 資格試験学習

など、さまざまな学習ケースに対応することができます。

今回のアプリは解説する内容が多いため、

  • 第1部 学習データ管理編
  • 第2部 学習クイズ実行編

の2部に分けて解説を行っています。

「学習データ」の登録と「学習の実行」の流れは下記の動画をご覧ください。

「学習したい内容」を自由に登録して「覚えたいこと」だけを覚えていくことができます。

アプリの画面構成

今回作成するアプリの画面は、

  • メイン画面(index.html)
  • 管理画面(manage.html)
  • 学習画面(learning.html)

の3つの画面で構成されています。

「メイン画面」から「管理画面」と「学習画面」に遷移することができますが、

  1. 「管理画面」で「学習データ」を登録する
  2. 「学習画面」で「クイズ」に答えながらで学習を行う

という手順で、学習を進めていきます。

「メイン画面」の作成方法

「メイン」画面は、「HTML・CSS」のみで構成されていて、「それぞれの画面へのリンク」が表示されているシンプルなものとなっています。

<!DOCTYPE html>
<html lang="ja">
	<head>
		<meta charset="UTF-8">
		<title>単語帳風クイズラーニングアプリ-メイン-</title>
		<link rel="stylesheet" href="common.css">
		<link rel="stylesheet" href="index.css">
	</head>
	<body>
		<div id="wrap_frame">
			<header>
				<img src="./images/title.png">
			</header>
			<a href="learning.html">学習開始</a>
			<a href="manage.html">学習データ管理</a>
		</div>
	</body>
</html>

学習データ登録

「管理画面」から「学習データ」を登録することができます。

「execRegist」関数では、下記のような処理を行っています。

/**
 * データ登録実行
 */
function execRegist(){
	clearErrors();  //エラーデータ表示をクリア
	let contents = getElmId('contents').value;  //「問題」
	let select_1 = getElmId('select_1').value;  //「選択肢1」
	let select_2 = getElmId('select_2').value;  //「選択肢2」
	let select_3 = getElmId('select_3').value;  //「選択肢3」
	let select_4 = getElmId('select_4').value;  //「選択肢4」
	let answer = getElmId('answer').value;      //「正解」

	//「配列データ」を作成
	let regist_data = {
		'contents': contents,
		'select_1': select_1,
		'select_2': select_2,
		'select_3': select_3,
		'select_4': select_4,
		'answer': answer,
		'challenge_count': 0,
		'success_count': 0
	} 

	let chk_result = checkRegistData(regist_data);  //「登録」データのチェック処理

	if(chk_result === false){
		g_learning_data.push(regist_data);
		saveLearningData();  //「学習データ」をセーブ
		dispLearningData();  //「学習データ」を表示

		getElmId('contents').value = '';  //「問題」
		getElmId('select_1').value = '';  //「選択肢1」
		getElmId('select_2').value = '';  //「選択肢2」
		getElmId('select_3').value = '';  //「選択肢3」
		getElmId('select_4').value = '';  //「選択肢4」
		getElmId('answer').value = '1';   //「正解」
		alert('クイズデータを登録しました。');
	}
}

「入力データ」のチェック後、問題が無ければ「saveLearningData」関数でブラウザの「ローカルストレージ」にデータを保存しています。

「ローカルストレージ」については、

→「ローカルストレージ」

をご参考ください。

「アプリ内で利用している学習データ(配列)」は「JSON」に変換して「ローカルストレージ」へ保存を行っています。

「ローカルストレージ」に保存する「saveLearningData」関数は下記のようになります。

/**
 * 「学習データ」をセーブ
 */
function saveLearningData(){
	let save_json = JSON.stringify(g_learning_data);    //「学習データ(配列)」を「JSON」へ変換
	localStorage.setItem('learning_data', save_json);   //「ローカルストレージ」へセーブ
}

「saveLearningData」関数の中で「学習データの配列」を「JSON」へ変換していますが、Javascriptで定義されている「JSONオブジェクト」の「stringifyメソッド」で変換を行っています。

登録すると、画面下部に登録した「学習データ(クイズ)」の一覧が表示されます。

学習データ編集

「登録データ一覧」の「編集」ボタンをクリックすると、「dispEdit」関数が実行されます。

「dispEdit」関数の内容は下記のようになり、「学習データ編集用画面」を表示するための処理が実行されています。

/**
 * 「編集データ」を表示
 */
function dispEdit(e){
	clearEditAnswerSelection();  //「回答項目」の選択をクリア
	let data_id = e.target.dataset.id;
	let ld = g_learning_data[data_id];
	getElmId('edit_contents').value = ld['contents'];  //「問題」
	getElmId('edit_select_1').value = ld['select_1'];  //「選択肢1」
	getElmId('edit_select_2').value = ld['select_2'];  //「選択肢2」
	getElmId('edit_select_3').value = ld['select_3'];  //「選択肢3」
	getElmId('edit_select_4').value = ld['select_4'];  //「選択肢4」
	getElmId('edit_answer').options[parseInt(ld['answer'])-1].selected = true;  //「正解」
	getElmId('edit').dataset.id = data_id;
	dispUI('edit_data');
}

「学習データ」の編集後、「編集実行」ボタンをクリックします。

「execEdit」関数の内容は下記のようになり、「配列内の学習データ」を変更し、「ローカルストレージ」に保存する処理が実行されています。

/**
 * 「編集」を実行
 */
function execEdit(e){
	let data_id = e.target.dataset.id;  //「データID」を取得
	let edit_data = []; 
	edit_data['contents'] = getElmId('edit_contents').value;  //「問題」
	edit_data['select_1'] = getElmId('edit_select_1').value;  //「選択肢1」
	edit_data['select_2'] = getElmId('edit_select_2').value;  //「選択肢2」
	edit_data['select_3'] = getElmId('edit_select_3').value;  //「選択肢3」
	edit_data['select_4'] = getElmId('edit_select_4').value;  //「選択肢4」
	edit_data['answer'] = getElmId('edit_answer').value;      //「正解」
	let chk_result = checkRegistData(edit_data);  //「登録」データのチェック処理

	if(chk_result === false){
		g_learning_data[data_id]['contents'] = edit_data['contents'];  //「問題」
		g_learning_data[data_id]['select_1'] = edit_data['select_1'];  //「選択肢1」
		g_learning_data[data_id]['select_2'] = edit_data['select_2'];  //「選択肢2」
		g_learning_data[data_id]['select_3'] = edit_data['select_3'];  //「選択肢3」
		g_learning_data[data_id]['select_4'] = edit_data['select_4'];  //「選択肢4」
		g_learning_data[data_id]['answer'] = edit_data['answer'];      //「正解」
		saveLearningData();     //「学習データ」をセーブ
		dispLearningData()      //「学習データ」を表示
		dispUI('regist_data');  //「指定UI」を表示
		alert("学習データを更新しました。");
	}
}

「学習データ」の編集では、複数存在している「学習データ」からどのデータを編集するのかを識別する必要があります。

「編集実行」ボタンのHTMLは、

<button class="edit_btn" data-id="0">編集</button>

のようになっていますが、学習データごとに「data-id属性」に「異なるID値」を設定することで、どの学習データを編集するのかを識別することができます。

この「ID値」は、「execEdit」関数内の、

let data_id = e.target.dataset.id;  //「データID」を取得

の「e.target.dataset.id」の部分で「data-id属性」の「ID値」を取得しています。

この「ID値」はアプリ内で保存している「学習データ(配列)」の「要素番号」のため、その要素番号の配列データを変更しています。

データ削除

「登録データ一覧」の「削除」ボタンをクリックすると、「deleteData」関数が実行されます。

「deleteData」関数の内容は下記のようになり、「配列内の学習データ」を削除し、「ローカルストレージ」に保存する処理が実行されています。

/**
 * 「学習データ」削除
 */
function deleteData(e){
	if(confirm('本当に削除しますか?')){
		g_learning_data.splice(e.target.dataset.id,1);  //「学習データ(配列)」からデータを削除
		saveLearningData(learning_data);                //「学習データ」をセーブ
		dispLearningData();                             //「学習データ」を表示
	}
}

「データの削除」でも編集と同様に「data-id属性」の「ID値」を取得し、データを識別しています。

エクスポート

「学習データ」は、ブラウザの「ローカルストレージ」に保存されていますが、「別のブラウザを使いたい!」または「別のPCで学習したい!」といった場合に、「インポート・エクスポート」機能を利用することで、簡単に学習データを「別のブラウザ」や「別のPC」に移行することができます。

それでは、まず学習データの「エクスポート」の方法を見ていきましょう。

まず、「メイン画面」の「エクスポート」ボタンをクリックします。

「displayExportScreen」関数の内容は下記のようになり、「エクスポート画面」を表示する処理が実行されています。

/**
 * 「エクスポート」画面を表示
 */
function displayExportScreen(){
	dispUI('export_data');
}

この関数が実行されると、下図のように「エクスポート画面」が表示されます。

「execExport」関数は、下記のようになります。

/**
 * 「エクスポート」処理を実行
 */
function execExport(){
	getElmId('export_contents').value = localStorage.getItem('learning_data');  //「ローカルストレージ」からデータをロード
	alert("学習データ(JSON)をエクスポートしました。");
}

「ローカルストレージ」から取得したデータを、「エクスポート画面」に表示する処理が実行されています。

インポート

次に学習データの「インポート」の方法をご説明していきたいと思います。

「displayImportScreen」関数の内容は下記のようになり、「インポート画面」を表示する処理が実行されています。

/**
 * 「インポート」画面を表示
 */
function displayImportScreen(){
	dispUI('import_data');
}

「エクスポート」した「JSONデータ」をペーストし、「インポート実行」ボタンをクリックする。

動画で作成した英単語の「JSONデータ」は下記のようになります。

[{"contents":"下記の選択肢の中で、「絶対に」を表す単語はどれですか?","select_1":"extreme","select_2":"definitely","select_3":"efficiently","select_4":"realistic","answer":"2","challenge_count":0,"success_count":0},{"select_1":"even if","select_2":"even when","select_3":"example","select_4":"there","answer":"1","challenge_count":0,"success_count":0,"contents":"下記の選択肢の中で、「例え~だとしても」を表す単語はどれですか?"},{"contents":"下記の選択肢の中で、「一時的な」を表す単語はどれですか?","select_1":"spot","select_2":"hold","select_3":"test","select_4":"temporary","answer":"4","challenge_count":0,"success_count":0},{"contents":"下記の選択肢の中で、「混乱させる」を表す単語はどれですか?","select_1":"confuse","select_2":"panic","select_3":"histery","select_4":"hang up","answer":"1","challenge_count":0,"success_count":0},{"contents":"下記の選択肢の中で、「説明」を表す単語はどれですか?","select_1":"distance","select_2":"complain","select_3":"teach","select_4":"manual","answer":"2","challenge_count":0,"success_count":0}]	

このデータをコピー&ペーストで「インポート」すると、「英単語クイズ」が実行できます。

「管理ページ(manage.html)」のプログラムコード

<!DOCTYPE html>
<html lang="ja">
	<head>
		<meta charset="UTF-8">
		<title>単語帳風クイズラーニングアプリ-データ管理-</title>
		<link rel="stylesheet" href="common.css">
		<link rel="stylesheet" href="manage.css">
		<script src="common.js"></script>
		<script>
			let g_learning_data = null; //学習データ格納用

			/**
			  * データ登録実行
			  */
			function execRegist(){
				clearErrors();  //エラーデータ表示をクリア
				let contents = getElmId('contents').value;  //「問題」
				let select_1 = getElmId('select_1').value;  //「選択肢1」
				let select_2 = getElmId('select_2').value;  //「選択肢2」
				let select_3 = getElmId('select_3').value;  //「選択肢3」
				let select_4 = getElmId('select_4').value;  //「選択肢4」
				let answer = getElmId('answer').value;      //「正解」

				//「配列データ」を作成
				let regist_data = {
					'contents': contents,
					'select_1': select_1,
					'select_2': select_2,
					'select_3': select_3,
					'select_4': select_4,
					'answer': answer,
					'challenge_count': 0,
					'success_count': 0
				} 

				let chk_result = checkRegistData(regist_data);  //「登録」データのチェック処理

				if(chk_result === false){
					g_learning_data.push(regist_data);
					saveLearningData();  //「学習データ」をセーブ
					dispLearningData();  //「学習データ」を表示

					getElmId('contents').value = '';  //「問題」
					getElmId('select_1').value = '';  //「選択肢1」
					getElmId('select_2').value = '';  //「選択肢2」
					getElmId('select_3').value = '';  //「選択肢3」
					getElmId('select_4').value = '';  //「選択肢4」
					getElmId('answer').value = '1';   //「正解」
					alert('クイズデータを登録しました。');
				}
			}

			/**
			  *「学習データ」を表示
			  */
			function dispLearningData(){
				var html = "";   
				var data_id = 0;
				if(g_learning_data.length > 0){
					g_learning_data.forEach(data => {
						html += createTableData(data,data_id);
						data_id++;
					});
				}

				let table_header_html = "<caption>登録データ(" + g_learning_data.length + "件)</caption><tr><th>問題</th><th>選択肢</th><th>正解番号</th><th>正解数/問題チャレンジ回数</th><th>編集</th><th>削除</th>"
				getElmId('learning_data').innerHTML = table_header_html + html;
				setListerToDeleteBtn();  //「学習データ」削除用イベントリスナー設定
				setListerToEditBtn();    //「学習データ」編集用イベントリスナー設定
			}

			/**
			  * テーブルデータ作成
			  */
			function createTableData(learning_data, data_id){
				let success_rate = 0;
				if( parseInt(learning_data['challenge_count']) !== 0){
					success_rate = Math.round(learning_data['success_count'] / learning_data['challenge_count'] * 100);
				}
				let html = "<tr>";
				html += "<td>" + learning_data['contents']; + "</td>";
				html += "<td>";
				html += "<ol>";
				html += "<li>"+learning_data['select_1']+"</li>";
				html += "<li>"+learning_data['select_2']+"</li>";
				html += "<li>"+learning_data['select_3']+"</li>"; 
				html += "<li>"+learning_data['select_4']+"</li>";
				html += "</ol>";
				html += "</td>";
				html += "<td>" + learning_data['answer']; + "</td>";
				html += "<td>" + learning_data['success_count'] +"/"+learning_data['challenge_count'] + "<br> (正解率:" + success_rate +"%)</td>";
				html += "<td><button class='edit_btn' data-id='" + data_id +"'>編集</button></td>";
				html += "<td><button class='delete_btn' data-id='" + data_id +"'>削除</button></td>";
				html += "</tr>";
				return html;
			}

			/**
			  * 「学習データ」削除用イベントリスナー設定
			  */
			function setListerToDeleteBtn(){
				let deleteBtns = getElmClass("delete_btn");
				if(deleteBtns.length !== 0){
					deleteBtns = Array.prototype.slice.call(deleteBtns);
					deleteBtns.forEach(data => {
						data.addEventListener("click", deleteData, false);
					});
				}
			}

			/**
			  * 「学習データ」削除
			  */
			function deleteData(e){
				if(confirm('本当に削除しますか?')){
					g_learning_data.splice(e.target.dataset.id,1);  //「学習データ(配列)」からデータを削除
					saveLearningData(learning_data);                //「学習データ」をセーブ
					dispLearningData();                             //「学習データ」を表示
				}
			}

			/**
			  * 「学習データ」編集用イベントリスナー設定
			  */
			function setListerToEditBtn(){
				let editBtns = getElmClass("edit_btn");
				if(editBtns.length !== 0){
					editBtns = Array.prototype.slice.call(editBtns);
					editBtns.forEach(data => {
						data.addEventListener("click", dispEdit, false);
					});
				}
			}

			/**
			  * 「編集データ」を表示
			  */
			function dispEdit(e){
				clearEditAnswerSelection();  //「回答項目」の選択をクリア
				let data_id = e.target.dataset.id;
				let ld = g_learning_data[data_id];
				getElmId('edit_contents').value = ld['contents'];  //「問題」
				getElmId('edit_select_1').value = ld['select_1'];  //「選択肢1」
				getElmId('edit_select_2').value = ld['select_2'];  //「選択肢2」
				getElmId('edit_select_3').value = ld['select_3'];  //「選択肢3」
				getElmId('edit_select_4').value = ld['select_4'];  //「選択肢4」
				getElmId('edit_answer').options[parseInt(ld['answer'])-1].selected = true;  //「正解」
				getElmId('edit').dataset.id = data_id;
				dispUI('edit_data');
			}

			/**
			  * 「回答項目」の選択をクリア
			  */
			function clearEditAnswerSelection(){
				for(var i = 0; i < 4; i++){
					getElmId('edit_answer').options[i].selected = false;
				}
			}

			/**
			  * 「編集」を実行
			  */
			function execEdit(e){
				let data_id = e.target.dataset.id;  //「データID」を取得
				let edit_data = []; 
				edit_data['contents'] = getElmId('edit_contents').value;  //「問題」
				edit_data['select_1'] = getElmId('edit_select_1').value;  //「選択肢1」
				edit_data['select_2'] = getElmId('edit_select_2').value;  //「選択肢2」
				edit_data['select_3'] = getElmId('edit_select_3').value;  //「選択肢3」
				edit_data['select_4'] = getElmId('edit_select_4').value;  //「選択肢4」
				edit_data['answer'] = getElmId('edit_answer').value;      //「正解」
				let chk_result = checkRegistData(edit_data);  //「登録」データのチェック処理

				if(chk_result === false){
					g_learning_data[data_id]['contents'] = edit_data['contents'];  //「問題」
					g_learning_data[data_id]['select_1'] = edit_data['select_1'];  //「選択肢1」
					g_learning_data[data_id]['select_2'] = edit_data['select_2'];  //「選択肢2」
					g_learning_data[data_id]['select_3'] = edit_data['select_3'];  //「選択肢3」
					g_learning_data[data_id]['select_4'] = edit_data['select_4'];  //「選択肢4」
					g_learning_data[data_id]['answer'] = edit_data['answer'];      //「正解」
					saveLearningData();     //「学習データ」をセーブ
					dispLearningData()      //「学習データ」を表示
					dispUI('regist_data');  //「指定UI」を表示
					alert("学習データを更新しました。");
				}
			}

			/**
			  * 「指定UI」を表示
			  */
			function dispUI(val){
				getElmId('regist_data').style.display = 'none';
				getElmId('edit_data').style.display = 'none';
				getElmId('learning_data').style.display = 'none';
				getElmId('import_data').style.display = 'none';
				getElmId('export_data').style.display = 'none';

				getElmId(val).style.display= 'block';

				if(val === "regist_data"){
					getElmId('learning_data').style.display = 'block';
				}
			}

			/**
			  * 「管理ボタン」クリック時の処理
			  */
			function clickManageBtn(){
				dispLearningData();  //「学習データ」を表示
				dispUI('regist_data');
			}

			/**
			  * 「インポート」画面を表示
			  */
			function displayImportScreen(){
				dispUI('import_data');
			}

			/**
			  * 「エクスポート」画面を表示
			  */
			function displayExportScreen(){
				dispUI('export_data');
			}

			/**
			  * 「インポート」処理を実行
			  */
			function execImport(){
				let import_json = getElmId('import_contents').value; 
				if(confirm('現在保存されている学習データは削除されます。本当にインポートしますか?')){
					let jsObj = getJson(import_json);
					if(jsObj !== false){
						g_learning_data = jsObj;
						saveLearningData();  //「学習データ」をセーブ
						alert("学習データ(JSON)をインポートしました。");
					} else {
						alert("このデータはインポートできません。");
					}   
				}
			}

			/**
			  * 「エクスポート」処理を実行
			  */
			function execExport(){
				getElmId('export_contents').value = localStorage.getItem('learning_data');  //「ローカルストレージ」からデータをロード
				alert("学習データ(JSON)をエクスポートしました。");
			}

			/**
			  * 「空」判定処理
			  */
			function isEmpty(val){
				if(val.length === 0){ return true; }
				return false;
			}


			/**
			  * 「JSON」データの取得処理
			  */
			function getJson(data) {
				let json;
				try {
					json = JSON.parse(data);
				} catch (e) {
					return false;
				}
				return json;
			}

			/**
			  * 「登録」データのチェック処理
			  */
			function checkRegistData(data){
				let error_flg = false;
				if(isEmpty(data['contents'])){
					getElmId('contents_error').innerHTML +="<li>学習内容を入力してください。</li>" ;
					getElmId('contents_error').style.visibility = "visible";  
					error_flg = true;
				}
	
				for ( var i = 1; i <= 4; i++){
					if(isEmpty(data['select_' + i])){
						getElmId('select_error').innerHTML +="<li>選択肢"+i+"を入力してください。</li>" ;
						getElmId('select_error').style.visibility = "visible";  
						error_flg = true;
					}
				}
				return error_flg;
			}

			/**
			  * エラーデータ表示をクリア
			  */
			function clearErrors(){
				getElmId('contents_error').innerHTML = "" ;
				getElmId('contents_error').style.visibility = "hidden"; 

				getElmId('select_error').innerHTML = "" ;
				getElmId('select_error').style.visibility = "hidden"; 
			}

			window.onload = function () {
				getElmId('regist').addEventListener('click', execRegist, false);
				getElmId('edit').addEventListener('click', execEdit, false);
				getElmId('hnav_manage_btn').addEventListener('click', clickManageBtn, false);
				getElmId('hnav_import_btn').addEventListener('click', displayImportScreen, false);
				getElmId('hnav_export_btn').addEventListener('click', displayExportScreen, false);
				getElmId('import_btn').addEventListener('click', execImport, false);
				getElmId('export_btn').addEventListener('click', execExport, false);
				loadLearningData();
				dispLearningData();  //「学習データ」を表示
				dispUI("regist_data");
			}
		</script>
	</head>
	<body>
		<div id="wrap_frame">
			<header>
				<img src="./images/title.png">
				<nav>
					<ul>
						<li><button id="hnav_manage_btn">管理</button></li>
						<li><button id="hnav_import_btn">インポート</button></li>
						<li><button id="hnav_export_btn">エクスポート</button></li>
					</ul>
				</nav>
			</header>
			<section id="regist_data">
				<h2>学習データ登録</h2>
				<p>問題</p>
				<textarea id="contents" rows="8" cols="80"></textarea>
				<ul id='contents_error' class="error"></ul>
				<p>選択項目</p>
				<ul>
					<li>選択1:<input type="text" id="select_1" value=""></li>
					<li>選択2:<input type="text" id="select_2" value=""></li>
					<li>選択3:<input type="text" id="select_3" value=""></li>
					<li>選択4:<input type="text" id="select_4" value=""></li>
				</ul>
				<p>正解項目</p>
				<select id="answer">
					<option value="1">選択1</option>
					<option value="2">選択2</option>
					<option value="3">選択3</option>
					<option value="4">選択4</option>
				</select>
				<ul id='select_error' class="error"></ul>
				<button id="regist">登録</button>
			</section>
			<section id="edit_data">
				<h2>学習データ編集</h2>
				<p>問題</p>
				<textarea id="edit_contents" rows="8" cols="80"></textarea>
				<ul id='contents_error' class="error"></ul>
				<p>選択項目</p>
				<ul>
					<li>選択1:<input type="text" id="edit_select_1" value=""></li>
					<li>選択2:<input type="text" id="edit_select_2" value=""></li>
					<li>選択3:<input type="text" id="edit_select_3" value=""></li>
					<li>選択4:<input type="text" id="edit_select_4" value=""></li>
				</ul>
				<p>正解項目</p>
				<select id="edit_answer">
					<option value="1">選択1</option>
					<option value="2">選択2</option>
					<option value="3">選択3</option>
					<option value="4">選択4</option>
				</select>
				<ul id='select_error' class="error"></ul>
				<button id="edit" data-id="0">編集実行</button>
			</section>
			<section id="import_data">
				<h2>インポート</h2>
				<p>インポートする「JSONデータ」を入力してください。</p>
				<textarea id="import_contents" rows="8" cols="80"></textarea>
				<button id="import_btn">インポート実行</button>
			</section>
			<section id="export_data">
				<h2>エクスポート</h2>
				<textarea id="export_contents" rows="8" cols="80"></textarea>
				<button id="export_btn">エクスポート実行</button>
			</section>
			<hr>
			<table id="learning_data">
				
			</table>
			<a href="index.html" id="to_main">メイン画面へ戻る</a>
		</div>
	</body>
</html>

→「第2部 ラーニングクイズアプリ~学習クイズ実行~」へ

HOMEへ