プログラミング初心者のための「単語帳風クイズラーニングアプリ」開発入門~第1部 学習データ管理編~
今回は、「単語帳風クイズラーニングアプリ」の作り方をご紹介していきたいと思います。
このアプリは「単語帳」のように、「自分で学びたい内容」を入力できて、気軽に学習を行うことができるため、
- 英語学習
- 学校のテスト勉強
- 資格試験学習
など、さまざまな学習ケースに対応することができます。
今回のアプリは解説する内容が多いため、
- 第1部 学習データ管理編
- 第2部 学習クイズ実行編
の2部に分けて解説を行っています。
「学習データ」の登録と「学習の実行」の流れは下記の動画をご覧ください。
「学習したい内容」を自由に登録して「覚えたいこと」だけを覚えていくことができます。
アプリの画面構成
今回作成するアプリの画面は、
- メイン画面(index.html)
- 管理画面(manage.html)
- 学習画面(learning.html)
の3つの画面で構成されています。

「メイン画面」から「管理画面」と「学習画面」に遷移することができますが、
- 「管理画面」で「学習データ」を登録する
- 「学習画面」で「クイズ」に答えながらで学習を行う
という手順で、学習を進めていきます。
「メイン画面」の作成方法
「メイン」画面は、「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>