2
1

【JavaScriptメイン】ラジオボタンを使用したクイズ【ランダム表示】

Posted at

1つ前の記事でクイズを作成したことを書いたのですが
ありがたいことにコメントでいただき、学習してみました。

コード紹介

データをJS側に持たせ、配列を駆使して問題や選択肢をランダムに表示できるようになっている。

<!DOCTYPE html>
<html lang='ja'>
<head>
  <meta charset='utf-8'>
  <title>quiz</title>
  <meta name='viewport' content='width=device-width, initial-scale=1'>
  <link rel='stylesheet' href='https://unpkg.com/ress/dist/ress.min.css'>
  <style>
  @charset 'UTF-8';

  html {
    font-size: 100%;
  }

  body {
    color: #383E45;
    font-size: 0.9rem;
    text-align: center;
  }

  .list {
    margin:auto;
  }

  ul {
    max-width: 500px;
    display: flex;
    margin: 20px auto;
  }

  li {
    list-style: none;
    margin:auto;
  }

  h2 {
    margin: 30px 0 10px 0;
  }

  #btn {
    display: block;
    text-align: center;
    text-decoration: none;
    width: 200px;
    margin: 70px auto;
    padding: 1rem 4rem;
    font-weight: bold;
    border: 2px solid #27acd9;
    color: #27acd9;
    border-radius: 100vh;
    transition: 0.5s;
  }
  #btn:hover {
    color: #fff;
    background-color: #27acd9;
  }
  </style>
</head>
<body>
  <div id='questBlock'></div>
  <div>
    <button id='btn'>解答</button>
    <p id='answer'></p>
  </div>
  <script>
    'use strict';

    const questions = [
      {
        title: '2024年のオリンピック開催都市は?',
        choices: [
          'パリ',
          'ロサンゼルス',
          'ベルリン',
          'ニューデリー',
        ],
      },
      {
        title: '世界で4番目に人口が多い国は?',
        choices: [
          'インドネシア',
          '中国',
          'イギリス',
          'アメリカ',
        ],
      },
      {
        title: '日本よりGDPが高い国は?',
        choices: [
          'ドイツ',
          'インド',
          'イタリア',
          'カナダ',
        ],
      },
    ];

    const questBlock = document.getElementById('questBlock');

    const tmp = structuredClone(questions);
    const arr = [];
    while (tmp.length) arr.push(tmp.splice(Math.random() * tmp.length, 1)[0]);

    arr.forEach((q, i) => {
      questBlock.insertAdjacentHTML('beforeend', `
        <h2>問題${1 + i}</h2>
        <p>${q.title}</p>
      `);
      const tmp = structuredClone(q.choices);
      const ul = document.createElement('ul');
      while (tmp.length) {
        const v = tmp.splice(Math.random() * tmp.length, 1)[0];
        ul.insertAdjacentHTML('beforeend',
          `<li><label><input type='radio' value='${v}' name='${i}'> ${v}</label></li>`);
      }
      questBlock.insertAdjacentElement('beforeend', ul);
    });

    document.getElementById('btn').addEventListener('click', () => {
      const rd = [...questBlock.querySelectorAll('[type=radio]:checked')];
      if (rd.length < arr.length) {
        alert('未解答の問題があります');
        return;
      }
      document.getElementById('answer').textContent =
        `あなたは${arr.length}問中${rd.filter((e, i) => e.value === arr[i].choices[0]).length}問正解です。`;
    });
  </script>
</body>
</html>

HTMLとCSSについて

<!DOCTYPE html>
<html lang='ja'>
<head>
  <meta charset='utf-8'>
  <title>quiz</title>
  <meta name='viewport' content='width=device-width, initial-scale=1'>
  <link rel='stylesheet' href='https://unpkg.com/ress/dist/ress.min.css'>
<!-- 〜〜〜〜省略〜〜〜〜 -->
</head>
<body>
  <div id='questBlock'></div>
  <div>
    <button id='btn'>解答</button>
    <p id='answer'></p>
  </div>
<!-- 〜〜〜〜省略〜〜〜〜 -->
</body>
</html>

body部分は各要素にid属性持たせ、問題文を入れるところ、解答ボタン、結果を表示するところと、とてもシンプル。

CSSはhead内のstyle部分だが、変更箇所ないため割愛。

Javascriptについて

  <script>
    'use strict';

    const questions = [
      {
        title: '2024年のオリンピック開催都市は?',
        choices: [
          'パリ',
          'ロサンゼルス',
          'ベルリン',
          'ニューデリー',
        ],
      },
      {
        title: '世界で4番目に人口が多い国は?',
        choices: [
          'インドネシア',
          '中国',
          'イギリス',
          'アメリカ',
        ],
      },
      {
        title: '日本よりGDPが高い国は?',
        choices: [
          'ドイツ',
          'インド',
          'イタリア',
          'カナダ',
        ],
      },
    ];

    const questBlock = document.getElementById('questBlock');

    const tmp = structuredClone(questions);
    const arr = [];
    while (tmp.length) arr.push(tmp.splice(Math.random() * tmp.length, 1)[0]);

    arr.forEach((q, i) => {
      questBlock.insertAdjacentHTML('beforeend', `
        <h2>問題${1 + i}</h2>
        <p>${q.title}</p>
      `);
      const tmp = structuredClone(q.choices);
      const ul = document.createElement('ul');
      while (tmp.length) {
        const v = tmp.splice(Math.random() * tmp.length, 1)[0];
        ul.insertAdjacentHTML('beforeend',
          `<li><label><input type='radio' value='${v}' name='${i}'> ${v}</label></li>`);
      }
      questBlock.insertAdjacentElement('beforeend', ul);
    });

    document.getElementById('btn').addEventListener('click', () => {
      const rd = [...questBlock.querySelectorAll('[type=radio]:checked')];
      if (rd.length < arr.length) {
        alert('未解答の問題があります');
        return;
      }
      document.getElementById('answer').textContent =
        `あなたは${arr.length}問中${rd.filter((e, i) => e.value === arr[i].choices[0]).length}問正解です。`;
    });
  </script>

処理の流れは、問題をオブジェクトとして配列にし、配列からランダムに問題と選択肢を抽出して順番に表示、解答ボタンを押したらユーザーの解答が正解かどうか確かめ、点数を出す。
また、未解答の問題がある場合はアラートも出すようにしている。

設問を配列にする

const questions = [
      {
        title: '2024年のオリンピック開催都市は?',
        choices: [
          'パリ',
          'ロサンゼルス',
          'ベルリン',
          'ニューデリー',
        ],
      },
      //〜〜〜省略〜〜〜
      ]

questionsには各問題が要素として配列になっている。
1つ1つの要素はオブジェクトで表現されていて、titleとchoicesという問題文と選択肢をプロパティとして持っている。
さらに、choicesも配列で、選択肢を要素に持っている。

choicesの配列は最初の要素が正解の選択肢。

問題をランダムに表示する

const questBlock = document.getElementById('questBlock');

    const tmp = structuredClone(questions);
    const arr = [];
    while (tmp.length) arr.push(tmp.splice(Math.random() * tmp.length, 1)[0]);

定数questBlockにhtmlの

<div id='questBlock'></div>

上記要素を取得。

const tmp = structuredClone(questions);

structuredCloneとは、ディープコピーを作成するメソッドである。

ディープコピーとはオリジナルの完全なコピーでありながら、独立しており、変更がオリジナルに干渉しない。
そのため、元データが保護されることで安全性が保たれるというメリットがある。(元データが他の場所で使われている場合や、うっかり変更してしまうというミスによるバグを防げる)

ここではquestionsに格納されている配列を完コピしてtmpに代入している。tmpの中身とquestionsの中身は一緒。

(定数名は自由につけることが可能だが、ここで使用されている「tmp」は「temporary」の略で、一時的なデータを格納しているということがわかりやすいらしい)

const arr = [];
while (tmp.length) arr.push(tmp.splice(Math.random() * tmp.length, 1)[0]);

const arr=[]は、空のarr配列を定義。

while文は条件がtrueの間、ループが続くが、今回は(tmp.length)でtmp配列に要素がある間(tmpの長さが0でない間)ループする。
初期値は3。tmp配列は問題を要素としてもっており、3問のクイズのため。

arr.pushは、arr配列の末尾に新しい要素を追加(push)する。

arrayオブジェクトのspliceメソッドは、配列のある要素から指定した要素を削除したり入れ替えたりする。

基本文法は下記。

配列名.splice(開始インデックス, 削除する要素数, 追加要素1, 追加要素2, ...)

今回は、tmp配列に対して、Math.random() * tmp.length番目のindexから1要素削除する。つまり、Math.random() * tmp.lengthの要素を削除。

具体的な数字を入れると、tmp=[1,2,3]という配列でarr.push(1, 1)[0]);の場合、tmp配列のインデックス1=2を削除し、配列[2]として返す。[2]の[0]番目の要素は2なのでarr配列には2が追加される。

今回、開始インデックスはMath.random() * tmp.lengthであり、ランダムに配列の要素を取り出している。

Math.randomは0以上1未満の乱数を発生させるメソッド。
ただし、0〜0.9999...なので1以上の整数を発生させたい場合はMath.random() * X(=上限になる数)という記述をする必要がある。

※Math.floorメソッドを使用しなくても、splice()は自動で整数に変換する。

arr.forEach((q, i) => {
      questBlock.insertAdjacentHTML('beforeend', `
        <h2>問題${1 + i}</h2>
        <p>${q.title}</p>
      `);

insertAdjacentHTMLは、呼び出し元のhtml要素部分に新しいhtml要素を挿入するメソッド。

基本構文:html要素.insertAdjacentHTML('新しいhtmlを挿入する位置を指定する文字列', 挿入したいhtml文字列);

--位置指定の種類--
'beforebegin' 要素の直前
'afterbegin' 最初の子要素の前
'beforeend' 最後の子要素の後
'afterend' 要素の直後

questBlock要素div〜/divの最後の子要素の後(/divの前)に
h2〜〜/pまでを挿入している

これをarr配列の各要素=qとインデックス=iを用いて順番に処理していく。

arrにランダムに入れた要素(問題)がオリジナルのquestionsと同じ順番だったと仮定して実際に要素を入れてみると・・・
配列の一番最初はインデックス0なのでi=0
問題番号は${1+0}で1
${q.title}はインデックス0=最初の問題のtitleなので「2024年のオリンピック開催都市は?」となる。

選択肢をランダムに表示する

const tmp = structuredClone(q.choices);
      const ul = document.createElement('ul');
      while (tmp.length) {
        const v = tmp.splice(Math.random() * tmp.length, 1)[0];
        ul.insertAdjacentHTML('beforeend',
          `<li><label><input type='radio' value='${v}' name='${i}'> ${v}</label></li>`);
      }
      questBlock.insertAdjacentElement('beforeend', ul);
    });

const tmp = structuredClone(q.choices);でそれぞれの問題の選択肢の完全コピーを作成。

const ul = document.createElement('ul');でul要素を新たに作り、tmp.lengthが0でない(tmp配列の要素が0でない)間、while{}処理を行う。

問題同様に、const v = tmp.splice(Math.random() * tmp.length, 1)[0];でランダムに選ばれた1つの選択肢を取り出し、以下に続く${v}の部分に入力する。

ul.insertAdjacentHTMLは、作成したul〜/ulの間にli〜/li要素を最後の子要素の後に入れている。

questBlock.insertAdjacentElement('beforeend', ul);では、ul要素をquestBlockに追加している。

これらの処理によってランダムに選択肢がliで追加される。

解答ボタンクリック動作と得点表示

document.getElementById('btn').addEventListener('click', () => {
      const rd = [...questBlock.querySelectorAll('[type=radio]:checked')];
      if (rd.length < arr.length) {
        alert('未解答の問題があります');
        return;
      }
      document.getElementById('answer').textContent =
        `あなたは${arr.length}問中${rd.filter((e, i) => e.value === arr[i].choices[0]).length}問正解です。`;
    });

document.getElementById('btn').addEventListener('click', () => {}
btn要素を取得し、クリックが発生したら{}内の処理を実施する。

const rd = [...questBlock.querySelectorAll('[type=radio]:checked')];

html要素questBlock内の全てのチェックされたラジオボタン(のオブジェクト)を取得し、配列にしている。

if文の条件が(rd.length < arr.length)なので
選択されたラジオボタンの数が設問数より少ない場合(設問は3なのにチェックが2つのときなど)に
「未解答の問題があります」とアラートを返す。

それ以外、つまり全てにチェックがある(rd.length === arr.length)時は、得点を表示。

`あなたは${arr.length}問中${rd.filter((e, i) => e.value === arr[i].choices[0]).length}問正解です。`;
    });

arr.length 設問数 つまり3

rd.filterはrd(チェックした解答の配列)をフィルターにかけ、e.value(rdの要素のvalue)とarr[i].choices[0] (arr配列のchoicesの最初の文字)が一致しているlength(数)を返す。

まとめ

js側にデータを持たせることで変更がかけやすく、ランダムに表示するなど機能の拡張性も高められるということがわかりました。

完コピ作成して元データの保護という観点は全くなく、実務の場合を想定して作っていく必要性も感じました。(実務経験なしなので完全には難しいですが・・・)

作りながら学ぶのが早いとエンジニア界隈?でよく言われていますが、その通りで、結局アウトプットの時にまた調べるので、手を動かし、実際に動かしてみるのが一番早いですね。

様々なメソッドが学べてとても参考になりました。読み解くのも楽しかったです。

ありがとうございました。

2
1
5

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1