プログラミング初心者のための「単語帳風クイズラーニングアプリ」開発入門~第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>