JavaScriptでアプリを作成しました【4】【シンプルなクイズアプリ】

##はじめに

####学習するに至った経緯

2020年より、未経験からエンジニアへの転職を目指し、某プログラミングスクールへ通う。入学後、『Ruby』を未経験から学ぶ人が多いのと『Ruby』の求人が思っていた以上に少ないので、卒業後、フロントエンドのエンジニアを目指す事に。
Javascriptの学習した事を言語化し、認識の深化による備忘録として記載。

##【仕様】

##【作成にあたり学習した事】
- イベントの処理方法 
- ユーザーインプットの取り扱い
- DOM操作 
- フィードバックの返し方 
- スコアのトラック方法（たとえばクライアントサイドに保存）

##JavaScriptクイズの基本構造

####① JavaScriptクイズの基本構造
- divタグにクイズを格納
- buttonタグでクイズを送信
- divタグに結果を表示

```index.html
<div id="quiz"></div>
<button id="submit">Submit Quiz</button>
<div id="result"></div>
```
####② HTMLタグを選択し、参照を変数に格納する

```main.js
const quizContainer = document.getElementById('quiz');
const resultsContainer = document.getElementById('results');
const submitButton = document.getElementById('submit');
```

####③ クイズを生成する機能、結果を表示する機能、それを統合する機能を作る

中身はあとで作るので、ここでは関数名を記述。クイズを生成して結果を表示する関数で、buildQuiz関数はただちに実行し、showResults関数はユーザーがSubmitボタンを押したときに実行する。

```main.js
funciton buildQuiz(){}
function showResults(){}
// クイズをすぐに表示する
buildQuiz();
// 投稿すると結果が表示される
submitButton.addEventListener('click',showResults);
```

##クイズの質問を表示する
クイズの質問を表示させる。ここでは個々の質問をオブジェクトリテラルで表現し、その配列がクイズに含める質問全体で、配列を使うことで1問ずつ質問にアクセスするのが簡単になる。必要な数だけの質問と解答を書く。

```main.js
const myQuestions = [
  {
    question: "Who is the strongest?",
    answers: {
      a: "Superman",
      b: "The Terminator",
      c: "Waluigi, obviously"
    },
    correctAnswer: "c"
  },
  {
    question: "What is the best site ever created?",
    answers: {
      a: "SitePoint",
      b: "Simple Steps Code",
      c: "Trick question; they're both the best"
    },
    correctAnswer: "c"
  },
  {
    question: "Where is Waldo really?",
    answers: {
      a: "Antarctica",
      b: "Exploring the Pacific Ocean",
      c: "Sitting in a tree",
      d: "Minding his own business, so stop asking"
    },
    correctAnswer: "d"
  }
];
```

##質問を表示する。

```main.js
function buildQuiz(){
  //  HTML出力を保存する場所が必要。
  const output = [];
  // それぞれの質問に対して...
  myQuestions.forEach(
    (currentQuestion, questionNumber) => {
      // 回答の選択肢のリストを格納する。
      const answers = [];

      // そして、用意されたそれぞれの答えに対して...
      for(letter in currentQuestion.answers){

        // ...HTMLラジオボタンの追加
        answers.push(
          `<label>
            <input type="radio" name="question${questionNumber}" value="${letter}">
            ${letter} :
            ${currentQuestion.answers[letter]}
          </label>`
        );
      }
      // 質問とその回答を出力に追加する
      output.push(
        `<div class="question"> ${currentQuestion.question} </div>
        <div class="answers"> ${answers.join('')} </div>`
      );
    }
  );
  //最後に、出力リストを1つのHTML文字列にまとめ、ページに貼り付けます。
  quizContainer.innerHTML = output.join('');
}
```
##コードについて1行ずつ説明。
①　HTML出力を格納する変数outputを用意し、この変数に質問と解答の選択肢を入力する。次に、それぞれの質問のHTMLを生成する為に、forEachループで順番に質問を処理する。

```main.js
myQuestions.forEach( (currentQuestion, questionNumber) => {
  // 各質問に対して実行したいコードは以下の通り。
});
```
② ループの中のコードを説明。それぞれの質問に対してHTMLを生成し、解答の選択肢を格納する配列を用意する。解答の選択肢ごとにループを使ってHTMLのラジオボタンをテンプレートリテラルを使い生成する。解答のラジオボタンを作り終えたら、質問と解答のHTMLを出力に追加する。
テンプレートリテラルと埋め込み文字列を使って、質問と解答のdivを生成し、解答の選択肢はjoin関数で1つの文字列に結合してから、answers divに挿入する。

```main.js
// 回答の選択肢のリストを格納する。
const answers = [];
// 用意されたそれぞれの答えに対して...
for(letter in currentQuestion.answers){
  // ...htmlラジオボタンの追加
  answers.push(
    `<label>
      <input type="radio" name="question${questionNumber}" value="${letter}">
      ${letter} :
      ${currentQuestion.answers[letter]}
    </label>`
  );
}
// 質問とその回答を出力に追加する
output.push(
  `<div class="question"> ${currentQuestion.question} </div>
  <div class="answers"> ${answers.join('')} </div>`
);
```

各質問のHTMLができたら、全てを結合してページに表示し、buildQuiz関数が完成。

```main.js
quizContainer.innerHTML = output.join('');
```

##クイズの結果を表示する
① ループで順に解答をチェックして結果を表示するshowResults関数を作る。

```main.js
function showResults(){
  // クイズに答えてコンテナを集める
  const answerContainers = quizContainer.querySelectorAll('.answers');
  // ユーザーの回答を記録する
  let numCorrect = 0;
  // それぞれの質問に対して...
  myQuestions.forEach( (currentQuestion, questionNumber) => {

    // 選択された答えを見つける
    const answerContainer = answerContainers[questionNumber];
    const selector = 'input[name=question'+questionNumber+']:checked';
    const userAnswer = (answerContainer.querySelector(selector) || {}).value;
    // 答えが正しければ
    if(userAnswer===currentQuestion.correctAnswer){
      // 正解数に加算
      numCorrect++;
      // 答えを緑に染める
      answerContainers[questionNumber].style.color = 'lightgreen';
    }
    // 答えが間違っているか、空白の場合
    else{
      // 答えを赤く染める
      answerContainers[questionNumber].style.color = 'red';
    }
  });
  // 全回答数のうち正解数を表示
  resultsContainer.innerHTML = numCorrect + ' out of ' + myQuestions.length;
}
```

② クイズのHTMLから全ての解答コンテナを選択し、正答数と現在の質問に対してユーザーが選択した解答を格納する変数を用意する。

```main.js
// クイズに答えてコンテナを集める
const answerContainers = quizContainer.querySelectorAll('.answers');
//  ユーザーの回答を記録する
let numCorrect = 0;
```

③ 質問の解答をループを使って順番にチェックする。

```main.js
// それぞれの質問に対して...
myQuestions.forEach( (currentQuestion, questionNumber) => {
  // 選択された答えを見つける
  const answerContainer = answerContainers[questionNumber];
  const selector = `input[name=question${questionNumber}]:checked`;
  const userAnswer = (answerContainer.querySelector(selector) || {}).value;
  // 答えが正しければ
  if(userAnswer===currentQuestion.correctAnswer){
    // 正解数に加算
    numCorrect++;
    // 答えを緑に染める
    answerContainers[questionNumber].style.color = 'lightgreen';
  }
  // 答えが間違っているか、空白の場合
  else{
    // 答えを赤に染める
    answerContainers[questionNumber].style.color = 'red';
  }
});
```

④ ユーザーが選択した解答をHTMLから取得する方法。

```main.js
// 選択された答えを見つける
const answerContainer = answerContainers[questionNumber];
const selector = `input[name=question${questionNumber}]:checked`;
const userAnswer = (answerContainer.querySelector(selector) || {}).value;
```

現在の質問に対する解答コンテナを取得し、次の行で、チェックされているラジオボタンを取得するCSSセレクターを定義し、先ほど定義したanswerContainerのうちCSSセレクターに合致するものをJavaScriptのquerySelectorで取得する。どの解答のラジオボタンが選択されているのかを調べ、最後に.valueでその解答の値を取得する。


##不完全なユーザーインプットへの対応
無解答なら、存在しない値は取得できないので、.valueはエラーを起こす。そこでorを意味する||と空のオブジェクトを意味する{}を追加する。できあがったコードは次の処理を行う。

- ユーザーが選択した解答のエレメントへの参照を取得する。存在しなければ、空のオブジェクトを使う
- 上記の値を取得する

最終的に取得する値はユーザーの解答かundefinedで、これでユーザーが無解答でもクイズのクラッシュを防ぐ。


##解答を評価して結果を表示
① 解答をチェックするループにある下記のコードは、正答と誤答を処理する。
ユーザーの解答が正答と一致した場合には、正答数に1を足し、オプションで選択肢を緑色にし、誤答か無回答の場合には、こちらもオプションで選択肢を赤色する。

```main.js
// 答えが正しければ
if(userAnswer===currentQuestion.correctAnswer){
  // 正解数に加算
  numCorrect++;

  // 答えを緑に染める
  answerContainers[questionNumber].style.color = 'lightgreen';

// 答えが間違っているか、空白の場合
} else {
  // 答えを赤く染める
  answerContainers[questionNumber].style.color = 'red';
}
```
② 解答をチェックするループが終わったら、ユーザーが正答した質問の数を表示する。

```main.js
// 全回答数のうち正解数を表示
resultsContainer.innerHTML = `${numCorrect} out of ${myQuestions.length}`;
```
##ページ区切りを追加
質問を1問ずつ表示できるようにする。
そのためには以下の機能が必要。

① 質問を表示する機能と非表示にする機能
② クイズのナビゲーションボタンの作成

##### ① クイズのコンテナに質問を入れて、表示・非表示を切り替える。
(1) ナビゲーションボタンとクイズのコンテナを追加

```index.html
<div class="quiz-container">
  <div id="quiz"></div>
</div>
<button id="previous">Previous Question</button>
<button id="next">Next Question</button>
<button id="submit">Submit Quiz</button>
<div id="results"></div>
```
(2) buildQuiz関数にslideクラスを持つdivエレメントを追加して、作成済みの質問と解答のコンテナを配置する。

```index.html
output.push(
  `<div class="slide">
    <div class="question"> ${currentQuestion.question} </div>
    <div class="answers"> ${answers.join("")} </div>
  </div>`
);
```

(3)CSSを使って、スライドをレイヤーで重ねて表示する。
今回はz-indexとopacityで、スライドをフェードイン、アウトさせる。

```style.css
.slide{
  position: absolute;
  left: 0px;
  top: 0px;
  width: 100%;
  z-index: 1;
  opacity: 0;
  transition: opacity 0.5s;
}
.active-slide{
  opacity: 1;
  z-index: 2;
}
.quiz-container{
  position: relative;
  height: 200px;
  margin-top: 40px;
}
```
(4)ナビゲーションボタンへの参照と表示中のスライドを格納する変数を追加する。

```main.js
// pagination
const previousButton = document.getElementById("previous");
const nextButton = document.getElementById("next");
const slides = document.querySelectorAll(".slide");
let currentSlide = 0;
```
(5)スライドを表示する関数を作成する。

```main.js
function showSlide(n) {
//表示中のスライドからactive-slideクラスを削除して、非表示にする
  slides[currentSlide].classList.remove('active-slide');
//次のスライドにactive-slideクラスを追加して、表示する
  slides[n].classList.add('active-slide');
//表示中のスライド番号を更新する
  currentSlide = n;

//始めのスライドなら、Previous Slideボタンを非表示にする。それ以外なら表示する
  if(currentSlide===0){
    previousButton.style.display = 'none';
  }
  else{
    previousButton.style.display = 'inline-block';
  }
//最後のスライドなら、Next Slideボタンを非表示にして、Submitボタンを表示する。それ以外なら、Next Slideボタンを表示して、Submitボタンを非表示にする
  if(currentSlide===slides.length-1){
    nextButton.style.display = 'none';
    submitButton.style.display = 'inline-block';
  }
  else{
    nextButton.style.display = 'inline-block';
    submitButton.style.display = 'none';
  }
}
関数を定義した直後に、showSlide(0)を呼び出して始めのスライドを表示する
showSlide(0);
```

####② ナビゲーションボタンを作動させる関数を作成。
showSlide関数で、ナビゲーションボタンを使って前後のスライドへ移動できるようにする。

```main.js
function showNextSlide() {
  showSlide(currentSlide + 1);
}
function showPreviousSlide() {
  showSlide(currentSlide - 1);
}
previousButton.addEventListener("click", showPreviousSlide);
nextButton.addEventListener("click", showNextSlide);
```

##完成

```index.html
<h1>Quiz on Important Facts</h1>
<div class="quiz-container">
  <div id="quiz"></div>
</div>
<button id="previous">Previous Question</button>
<button id="next">Next Question</button>
<button id="submit">Submit Quiz</button>
<div id="results"></div>
```


```style.css
@import url(https://fonts.googleapis.com/css?family=Work+Sans:300,600);

body{
  font-size: 20px;
  font-family: 'Work Sans', sans-serif;
  color: #333;
  font-weight: 300;
  text-align: center;
  background-color: #f8f6f0;
}
h1{
  font-weight: 300;
  margin: 0px;
  padding: 10px;
  font-size: 20px;
  background-color: #444;
  color: #fff;
}
.question{
  font-size: 30px;
  margin-bottom: 10px;
}
.answers {
  margin-bottom: 20px;
  text-align: left;
  display: inline-block;
}
.answers label{
  display: block;
  margin-bottom: 10px;
}
button{
  font-family: 'Work Sans', sans-serif;
  font-size: 22px;
  background-color: #279;
  color: #fff;
  border: 0px;
  border-radius: 3px;
  padding: 20px;
  cursor: pointer;
  margin-bottom: 20px;
}
button:hover{
  background-color: #38a;
}

.slide{
  position: absolute;
  left: 0px;
  top: 0px;
  width: 100%;
  z-index: 1;
  opacity: 0;
  transition: opacity 0.5s;
}
.active-slide{
  opacity: 1;
  z-index: 2;
}
.quiz-container{
  position: relative;
  height: 200px;
  margin-top: 40px;
}
```


```main.js
(function(){
  // Functions
  function buildQuiz(){
    // variable to store the HTML output
    const output = [];

    // for each question...
    myQuestions.forEach(
      (currentQuestion, questionNumber) => {

        // variable to store the list of possible answers
        const answers = [];

        // and for each available answer...
        for(letter in currentQuestion.answers){

          // ...add an HTML radio button
          answers.push(
            `<label>
              <input type="radio" name="question${questionNumber}" value="${letter}">
              ${letter} :
              ${currentQuestion.answers[letter]}
            </label>`
          );
        }

        // add this question and its answers to the output
        output.push(
          `<div class="slide">
            <div class="question"> ${currentQuestion.question} </div>
            <div class="answers"> ${answers.join("")} </div>
          </div>`
        );
      }
    );

    // finally combine our output list into one string of HTML and put it on the page
    quizContainer.innerHTML = output.join('');
  }

  function showResults(){

    // gather answer containers from our quiz
    const answerContainers = quizContainer.querySelectorAll('.answers');

    // keep track of user's answers
    let numCorrect = 0;

    // for each question...
    myQuestions.forEach( (currentQuestion, questionNumber) => {

      // find selected answer
      const answerContainer = answerContainers[questionNumber];
      const selector = `input[name=question${questionNumber}]:checked`;
      const userAnswer = (answerContainer.querySelector(selector) || {}).value;

      // if answer is correct
      if(userAnswer === currentQuestion.correctAnswer){
        // add to the number of correct answers
        numCorrect++;

        // color the answers green
        answerContainers[questionNumber].style.color = 'lightgreen';
      }
      // if answer is wrong or blank
      else{
        // color the answers red
        answerContainers[questionNumber].style.color = 'red';
      }
    });

    // show number of correct answers out of total
    resultsContainer.innerHTML = `${numCorrect} out of ${myQuestions.length}`;
  }

  function showSlide(n) {
    slides[currentSlide].classList.remove('active-slide');
    slides[n].classList.add('active-slide');
    currentSlide = n;
    if(currentSlide === 0){
      previousButton.style.display = 'none';
    }
    else{
      previousButton.style.display = 'inline-block';
    }
    if(currentSlide === slides.length-1){
      nextButton.style.display = 'none';
      submitButton.style.display = 'inline-block';
    }
    else{
      nextButton.style.display = 'inline-block';
      submitButton.style.display = 'none';
    }
  }

  function showNextSlide() {
    showSlide(currentSlide + 1);
  }

  function showPreviousSlide() {
    showSlide(currentSlide - 1);
  }

  // Variables
  const quizContainer = document.getElementById('quiz');
  const resultsContainer = document.getElementById('results');
  const submitButton = document.getElementById('submit');
  const myQuestions = [
    {
      question: "Who invented JavaScript?",
      answers: {
        a: "Douglas Crockford",
        b: "Sheryl Sandberg",
        c: "Brendan Eich"
      },
      correctAnswer: "c"
    },
    {
      question: "Which one of these is a JavaScript package manager?",
      answers: {
        a: "Node.js",
        b: "TypeScript",
        c: "npm"
      },
      correctAnswer: "c"
    },
    {
      question: "Which tool can you use to ensure code quality?",
      answers: {
        a: "Angular",
        b: "jQuery",
        c: "RequireJS",
        d: "ESLint"
      },
      correctAnswer: "d"
    }
  ];

  // Kick things off
  buildQuiz();

  // Pagination
  const previousButton = document.getElementById("previous");
  const nextButton = document.getElementById("next");
  const slides = document.querySelectorAll(".slide");
  let currentSlide = 0;

  // Show the first slide
  showSlide(currentSlide);

  // Event listeners
  submitButton.addEventListener('click', showResults);
  previousButton.addEventListener("click", showPreviousSlide);
  nextButton.addEventListener("click", showNextSlide);
})();
```

##参考サイト
[Java Scriptで3択クイズを作ろう【プログラミング】]
(https://www.youtube.com/watch?v=9T6zL_iuKFE&t=44s)
[JavaScript入門者が初めて作る！シンプルなクイズアプリのチュートリアル]
(https://www.webprofessional.jp/simple-javascript-quiz/)