はじめに
本記事では ドットインストール さんの JavaScriptでミニアプリを作ってみよう の解説をしています。
1年ぶりくらいにやってみたら意外と面白いなーと思ったので載せます。
意外と苦戦したんで、間違っている箇所(特にスプレッド構文あたりが怪しい)、生ぬるい目で見守ってやってください。気づいたら、適宜記述を訂正していきます!
この記事の対象読者様
・ドットインストールをやってる、やったことのある人、該当の動画を見たことのある人
・Jsの練習をしたい人
・Jsの基礎を実践でどのように使えるのか、基礎文法と照らし合わせながら学びたい人。
3択クイズアプリの制作における要件とヒント
クイズを作成する
ー クイズ配列をオブジェクトで作成(初期設定)
シャッフル関数の実装
ー フィッシャー・ィエ〜ツさんのアルゴリズムで実装
ーー 乱数の生成と分割代入を使います
ーー その後スプレッド構文で、参照渡しのようなことをして新しい配列を作成します。
解答(li)をクリックした時の処理
ー 正誤判定をする関数を実行。
ーー 正誤に応じて背景色が変化する。
ーー nextボタンがactiveな状態になる。
ーー 解答すると、他の選択肢は選べない状態にする。
Nextボタンを押した時の処理
ー 次のクイズに移動
ーー この時、前のクイズが残らないようにsetQuiz関数内であらかじめ条件式を書いておく。
ー Nextボタンを無効化する。
Show Scoreボタンを押した時の処理
ー モーダルが降ってくる
ーー モーダルには1/3正解のような文章とReplayボタンを設置
感想とミス
- 前の選択肢が残らないようにする仕組みが巧妙。
- スプレッド構文で、新たな配列を作成し(「 データが格納されているメモリ空間のアドレス 」)変数が参照するオブジェクトのアドレスを逸らしているという理解をするのが難しい。
- isAnsweredの定義の場所を間違えました。
- btnのテキストをラストでshowScoreとする処理の場所を間違えました。
一応、全コード。Jsのみコメントつきで解説
'use strict';
const question = document.getElementById('question');
const choices = document.getElementById('choices');
const btn = document.getElementById('btn');
const result = document.querySelector('#result');
const p = document.querySelector('#result > p');
const quizSet = [
{q: 'What is A?', c: ['A0', 'A1', 'A2']},
{q: 'What is B?', c: ['B0', 'B1', 'B2']},
{q: 'What is C?', c: ['C0', 'C1', 'C2']},
];
//ここから記述
let currentNum = 0;
let score = 0;
let isAnswered;//undifinedで初期化。ここをfalseで初期化してしまうとバグになる。
//[0,1,2,3,4]という配列のlength = 5
function shuffle(arr) {
for(let i = quizSet.length - 1; i > 0; i--) { // - 1する理由はindex番号ではなく i = 4という「要素」を取り出したいから。
const j = Math.floor(Math.random() * (i + 1)); //(i + 1)は配列の要素数の長さ。
[arr[j], arr[i]] = [arr[i], arr[j]]; //分割代入の記述方法 [x, y] = [y, x];
}
return arr;
}
function checkAnswer(li) {
console.log(isAnswered);//1回目のクリックではundefined、なぜなら参照しているのは14行目のレキシカルスコープにあるisAnswered。
//だと思っていたが...setQuiz()のisAnswered = falseが実行されて。 1回目からfalseで渡ってくることにデバックして気づいた.....。
if(isAnswered) { //ここのisAnsweredがtrueではないので、通る。あとはsetQuiz()呼ばれる度にfalse
return;
}
isAnswered = true;
if(li.textContent === quizSet[currentNum].c[0]) {
li.classList.add('correct');
score++;
} else {
li.classList.add('wrong');
}
btn.classList.remove('disabled');
}
function setQuiz() {
isAnswered = false; //ここでfalseにして、checkAnswerの処理が実行可能になる。
const shuffledChoices = shuffle([...quizSet[currentNum].c]); //元配列を別のメモリ空間に格納。
//実はここで初期化をしている。(ulの中を空っぽにしている)。while文の後で1番目のクイズを設定している。
while(choices.firstChild !== null) { //nullでなければ
choices.removeChild(choices.firstChild); //全て消す
}
question.textContent = quizSet[currentNum].q;
shuffledChoices.forEach(choice => {
const li = document.createElement('li');
li.addEventListener('click', () => {
checkAnswer(li);
});
li.textContent = choice;
choices.appendChild(li);
});
// ここでbtn.textContentの内容を条件分岐で変更する必要がある。
// btnのaddEventListenerの中に記述するとクリックした時の挙動なので間違い。
if(currentNum === quizSet.length - 1) {
btn.textContent = 'Show score';
}
}
setQuiz();//実行 isAnswerd = falseになる
btn.addEventListener('click', () => {
btn.classList.add('disabled');
if(currentNum === quizSet.length - 1) { //currentNumはelseで都度変わるので、currentNum = 3とかしたらダメです。
result.classList.remove('hidden'); //sectionというモーダルにデフォルトでつけておいたhiddenクラスを外す
p.textContent = `Score: ${score} / ${quizSet.length}`;
} else {
currentNum++;//
setQuiz();
}
});
html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>Quiz</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<section class="container">
<p id="question"></p>
<ul id="choices"></ul>
<div id="btn" class="disabled">Next</div>
<section id="result" class="hidden">
<p></p>
<a href="">Replay</a>
</section>
</section>
<script src="main.js"></script>
</body>
</html>
css
body {
background: #efdec1;
font-size: 14px;
font-family: Verdana, sans-serif;
}
.container {
width: 400px;
margin: 8px auto;
background: #fff;
border-radius: 4px;
padding: 16px;
position: relative;
}
#result.hidden {
transform: translateY(-300px);
}
#result {
position: absolute;
top: 100px;
padding: 30px;
box-shadow: 0 4px 8px rgba(0 ,0 ,0, 0.4);
left: 0;
right: 0;
margin: 0 auto;
background-color: white;
text-align: center;
transition: 0.4s;
}
#question {
margin-bottom: 16px;
font-weight: bold;
}
#choices {
list-style: none;
padding: 0;
margin-bottom: 16px;
}
#choices > li {
border: 1px solid #ccc;
border-radius: 4px;
padding: 10px;
margin-bottom: 10px;
cursor: pointer;
}
#choices > li:hover {
background: #f8f8f8;
}
#result > a {
text-decoration: none;
}
#btn, #result > a {
background: #3498db;
padding: 8px;
border-radius: 4px;
cursor: pointer;
text-align: center;
color: #fff;
box-shadow: 0 4px 0 #2880b9;
}
#btn.disabled {
background: #ccc;
box-shadow: 0 4px 0 #bbb;
opacity: 0.7;
}
#choices > li.correct {
background-color: hotpink;
}
.correct::after {
content: 'correct!';
}
#choices > li.wrong {
background-color: darkslateblue;
}
.wrong::after {
content: 'wrong....';
}
終わりに
このほかにもミニアプリ作成の動画はいくつかあるので、記法を変えたりOOPで書いてみたりと、遊びながら勉強していきたいですーー