セレクトボックス 3 — 動的リスト生成

フォーム 中級

このコンポーネントについて

フォームの選択肢をHTMLにハードコードせず、外部から取得したデータを元にJavaScriptで動的に生成するパターンです。 選択肢の追加・変更をデータファイルだけで済ませられるため、HTMLを書き換えずに選択肢をメンテナンスできます。

このページでは Pattern 1:ドロップダウンリスト<select>)と Pattern 2:グリッドリスト(チェックボックスの横並び)の2パターンで、データ駆動の動的なUI生成を確認・コピペできます。 デモ内でデータセットを切り替えると、2つのパターンの選択肢が同時に更新されます。なお、このデモではデータの取得にJSONファイルと fetch() を使っていますが、実際の現場ではAPIレスポンスや変数に格納した配列でも同じ考え方で実装できます。

  • データからの動的生成 — 配列データをループしてDOMに展開する。HTMLに選択肢をベタ書きしないため、データさえ差し替えれば選択肢を丸ごと入れ替えられる
  • データセット切り替え — 複数の配列データを持ち、ボタンで使用するデータを動的に切り替えられる。初期表示はデータセット1
  • Pattern 1 — ドロップダウン<select><option> をデータから動的生成。決定ボタンで value・テキストを表示する
  • Pattern 2 — グリッドリスト — データからチェックボックスをグリッドレイアウトで生成。決定ボタンで全選択項目の value・テキストを一覧表示する

実装のポイント・注意点

fetch() は非同期処理です。 JSONを取得してからDOMに反映するまで少し時間がかかるため、.then() のコールバック内でレンダリング処理を行います。取得が完了する前にDOMを操作しようとすると何も表示されないため、処理の順序に注意してください。

ローカルファイルでの動作確認には簡易サーバーが必要です。 fetch()file:// プロトコルでは CORS エラーになるため、ブラウザで直接 index.html を開いても動きません。VS Code の「Live Server」拡張機能や npx serve などで簡易サーバーを起動してから開いてください。

データセット切り替え時は既存のDOMをクリアしてから再生成します。 Pattern 1 は先頭の「選択してください」option だけ残し、それ以降を削除してから新データで option を追加します。Pattern 2 はグリッドの innerHTML をクリアして再生成します。

チェックボックスのテキストを保持する方法。 Pattern 2 では動的に生成した <input> 要素に data-name 属性を付与し、表示名を保持します。決定ボタン押下時に cb.dataset.name でテキストを取得する設計です(parentElement.textContent.trim() でも取得できますが、data属性のほうが確実です)。

デモ

データセット:

Pattern 1 — ドロップダウンリスト

Pattern 2 — グリッドリスト(複数選択)

すべて選んでください(複数選択可)

サンプルソース

4つのファイルを同じフォルダに保存し、簡易サーバーで index.html を開くと動作確認できます。
ファイル構成:index.html / style.css / script.js / data/data.jsondata フォルダを作って中に配置)
保存時の文字コードは UTF-8 を指定してください。fetch()file:// では動作しないため、VS Code の Live Server 等で開いてください。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>動的リスト生成 サンプル</title>
  <link rel="stylesheet" href="./style.css">
</head>
<body>

<!-- データセット切り替え -->
<div class="fs3-switcher">
  <span class="fs3-switcher-label">データセット:</span>
  <button class="fs3-switcher-btn active" data-dataset="dataset1">データセット 1(都道府県)</button>
  <button class="fs3-switcher-btn" data-dataset="dataset2">データセット 2(業種)</button>
</div>

<!-- Pattern 1: ドロップダウンリスト -->
<div class="fs3-pattern">
  <p class="fs3-pattern-label">Pattern 1 — ドロップダウンリスト</p>
  <div class="fs3-p1-wrapper">
    <label class="fs3-select-label" for="fs3-select">選択してください</label>
    <div class="fs3-p1-row">
      <!-- JSONから動的に option を追加する -->
      <select id="fs3-select" class="fs3-select">
        <option value="">— 選択してください —</option>
      </select>
      <button class="fs3-submit-btn" id="fs3-select-submit">決定</button>
    </div>
    <div class="fs3-result" id="fs3-select-result" aria-live="polite"></div>
  </div>
</div>

<!-- Pattern 2: グリッドリスト(複数選択) -->
<div class="fs3-pattern">
  <p class="fs3-pattern-label">Pattern 2 — グリッドリスト(複数選択)</p>
  <div class="fs3-p2-wrapper">
    <p class="fs3-grid-label">すべて選んでください(複数選択可)</p>
    <!-- JSONからチェックボックスを動的生成する -->
    <div class="fs3-grid" id="fs3-grid"></div>
    <button class="fs3-submit-btn" id="fs3-grid-submit">決定</button>
    <div class="fs3-result" id="fs3-grid-result" aria-live="polite"></div>
  </div>
</div>

<script src="./script.js"></script>
</body>
</html>
/* === 動的リスト生成 サンプル ===
   :root の変数を書き換えると色を一括変更できます */
:root {
  --color-accent:  #2B7FE8;
  --color-text:    #1A2332;
  --color-border:  #D0D7E0;
  --color-muted:   #9AA5B4;
  --color-bg:      #F4F6F9;
  --color-danger:  #E65100;
}
* { box-sizing: border-box; }
body { font-family: sans-serif; color: var(--color-text); padding: 24px; max-width: 600px; }

/* データセット切り替え */
.fs3-switcher {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  margin-bottom: 24px;
}
.fs3-switcher-label { font-size: 13px; font-weight: 700; }
.fs3-switcher-btn {
  padding: 6px 14px;
  font-size: 13px;
  color: #5A6A7A;
  background: #fff;
  border: 1.5px solid var(--color-border);
  border-radius: 20px;
  cursor: pointer;
  transition: background 0.15s, border-color 0.15s, color 0.15s;
}
.fs3-switcher-btn:hover { background: var(--color-bg); }
.fs3-switcher-btn.active {
  background: var(--color-accent);
  border-color: var(--color-accent);
  color: #fff;
}

/* パターン共通 */
.fs3-pattern { margin-bottom: 32px; }
.fs3-pattern-label {
  font-size: 12px;
  font-weight: 700;
  color: var(--color-accent);
  letter-spacing: 0.04em;
  margin-bottom: 12px;
}

/* Pattern 1 */
.fs3-select-label { display: block; margin-bottom: 8px; font-size: 14px; font-weight: 700; }
.fs3-p1-row { display: flex; gap: 10px; align-items: center; }
.fs3-select {
  flex: 1;
  max-width: 280px;
  height: 40px;
  padding: 0 10px;
  font-size: 14px;
  border: 1.5px solid var(--color-border);
  border-radius: 8px;
  outline: none;
  transition: border-color 0.2s, box-shadow 0.2s;
}
.fs3-select:focus {
  border-color: var(--color-accent);
  box-shadow: 0 0 0 3px rgba(43, 127, 232, 0.12);
}

/* Pattern 2 */
.fs3-grid-label { font-size: 14px; font-weight: 700; margin-bottom: 10px; }
.fs3-grid { display: flex; flex-wrap: wrap; gap: 6px 0; margin-bottom: 12px; }
.fs3-grid-item { width: 160px; }
.fs3-grid-item label {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 14px;
  cursor: pointer;
  padding: 4px 0;
}
.fs3-grid-item input[type="checkbox"] {
  width: 16px;
  height: 16px;
  accent-color: var(--color-accent);
  cursor: pointer;
}

/* 決定ボタン */
.fs3-submit-btn {
  padding: 8px 20px;
  font-size: 14px;
  font-weight: 700;
  color: #fff;
  background: var(--color-accent);
  border: none;
  border-radius: 6px;
  cursor: pointer;
  transition: background 0.15s;
}
.fs3-submit-btn:hover { background: #1a6fd1; }

/* 結果エリア */
.fs3-result { margin-top: 14px; }
.fs3-result.has-value {
  padding: 12px 16px;
  border: 1.5px solid var(--color-border);
  border-radius: 8px;
  background: #fff;
}
.fs3-result.has-error { font-size: 13px; color: var(--color-danger); padding: 4px 0; }
.fs3-result-list {
  list-style: none;
  margin: 0;
  padding: 0;
  font-size: 13px;
  line-height: 1.9;
}
.fs3-result-list code {
  font-family: monospace;
  background: #E8EDF4;
  padding: 1px 5px;
  border-radius: 3px;
  font-size: 12px;
}

/* スマホ対応 */
@media (max-width: 480px) {
  .fs3-grid-item { width: calc(50% - 4px); }
}
var jsonData = null;
var currentDataset = 'dataset1';

// ページ読み込み時にJSONを取得して両パターンを初期化する
// ※ fetch() は file:// では動作しないため、簡易サーバーで開いてください
(function () {
  fetch('./data/data.json')
    .then(function (res) { return res.json(); })
    .then(function (data) {
      jsonData = data;
      renderSelect(data[currentDataset].items);
      renderGrid(data[currentDataset].items);
    })
    .catch(function (err) {
      console.error('data.json の読み込みに失敗しました。data/ フォルダを確認してください。', err);
    });
})();

// データセット切り替えボタン
document.querySelectorAll('.fs3-switcher-btn').forEach(function (btn) {
  btn.addEventListener('click', function () {
    if (btn.dataset.dataset === currentDataset) return;

    document.querySelectorAll('.fs3-switcher-btn').forEach(function (b) {
      b.classList.remove('active');
    });
    btn.classList.add('active');

    currentDataset = btn.dataset.dataset;
    var items = jsonData[currentDataset].items;
    renderSelect(items);
    renderGrid(items);
    clearResult('fs3-select-result');
    clearResult('fs3-grid-result');
  });
});

// Pattern 1: select の option をJSONから生成する
function renderSelect(items) {
  var select = document.getElementById('fs3-select');
  // 先頭の「選択してください」option 以外をすべて削除する
  while (select.options.length > 1) {
    select.remove(1);
  }
  for (var i = 0; i < items.length; i++) {
    var opt = document.createElement('option');
    opt.value = items[i].key;
    opt.textContent = items[i].name;
    select.appendChild(opt);
  }
}

// Pattern 2: チェックボックスグリッドをJSONから生成する
function renderGrid(items) {
  var grid = document.getElementById('fs3-grid');
  grid.innerHTML = '';
  for (var i = 0; i < items.length; i++) {
    var div = document.createElement('div');
    div.className = 'fs3-grid-item';
    var label = document.createElement('label');
    var cb = document.createElement('input');
    cb.type = 'checkbox';
    cb.value = items[i].key;
    // テキスト取得用に name を data 属性で保持する
    cb.dataset.name = items[i].name;
    label.appendChild(cb);
    label.appendChild(document.createTextNode(' ' + items[i].name));
    div.appendChild(label);
    grid.appendChild(div);
  }
}

// Pattern 1: 決定
document.getElementById('fs3-select-submit').addEventListener('click', function () {
  var select = document.getElementById('fs3-select');
  var resultEl = document.getElementById('fs3-select-result');

  if (!select.value) {
    showError(resultEl, '選択してください');
    return;
  }

  // selectedIndex で選択中 option のテキストを取得する
  var selectedText = select.options[select.selectedIndex].text;
  showValue(resultEl,
    '<ul class="fs3-result-list"><li>' +
    'value: <code>' + select.value + '</code>' +
    ' &nbsp;/&nbsp; テキスト: <code>' + selectedText + '</code>' +
    '</li></ul>'
  );
});

// Pattern 2: 決定
document.getElementById('fs3-grid-submit').addEventListener('click', function () {
  var grid = document.getElementById('fs3-grid');
  var resultEl = document.getElementById('fs3-grid-result');
  // チェック済みのチェックボックスをすべて取得する
  var checked = grid.querySelectorAll('input[type="checkbox"]:checked');

  if (checked.length === 0) {
    showError(resultEl, '1つ以上選んでください');
    return;
  }

  var html = '<ul class="fs3-result-list">';
  checked.forEach(function (cb) {
    html += '<li>';
    html += 'value: <code>' + cb.value + '</code>';
    html += ' &nbsp;/&nbsp; テキスト: <code>' + cb.dataset.name + '</code>';
    html += '</li>';
  });
  html += '</ul>';
  showValue(resultEl, html);
});

function showValue(el, html) { el.className = 'fs3-result has-value'; el.innerHTML = html; }
function showError(el, msg)  { el.className = 'fs3-result has-error'; el.textContent = msg; }
function clearResult(id) {
  var el = document.getElementById(id);
  el.className = 'fs3-result';
  el.innerHTML = '';
}

data フォルダを作成し、data.json として保存してください。

{
  "dataset1": {
    "label": "都道府県",
    "items": [
      { "key": "tokyo",      "name": "東京都" },
      { "key": "kanagawa",   "name": "神奈川県" },
      { "key": "osaka",      "name": "大阪府" },
      { "key": "aichi",      "name": "愛知県" },
      { "key": "fukuoka",    "name": "福岡県" },
      { "key": "hokkaido",   "name": "北海道" },
      { "key": "kyoto",      "name": "京都府" },
      { "key": "hiroshima",  "name": "広島県" },
      { "key": "miyagi",     "name": "宮城県" },
      { "key": "okinawa",    "name": "沖縄県" }
    ]
  },
  "dataset2": {
    "label": "業種",
    "items": [
      { "key": "it",            "name": "IT・ソフトウェア" },
      { "key": "finance",       "name": "金融・保険" },
      { "key": "retail",        "name": "小売・流通" },
      { "key": "medical",       "name": "医療・福祉" },
      { "key": "education",     "name": "教育・研究" },
      { "key": "manufacturing", "name": "製造業" },
      { "key": "construction",  "name": "建設・不動産" },
      { "key": "food",          "name": "飲食・ホテル" },
      { "key": "media",         "name": "メディア・広告" },
      { "key": "government",    "name": "公務員・行政" }
    ]
  }
}

AI用プロンプト

各パターンのプロンプトをコピーしてAIに渡すと、同様のコンポーネントを生成できます。

ChatGPTやClaudeにこのプロンプトを渡すと、同様のコンポーネントをゼロから生成・カスタマイズできます。JSONのデータ構造や選択肢の変更など、要件を追記して使うのがおすすめです。

※ このプロンプトを使ってもデモとまったく同じ動作にならない場合があります。AIの解釈や生成タイミングによって差が出ることをご了承ください。

💡 jQuery・Vue・React など特定のライブラリで実装したい場合は、プロンプトの末尾に「〇〇を使って実装してください」と追記してください。

Pattern 1 — ドロップダウンリスト(動的生成)

# セレクトボックス(動的リスト生成)作成依頼

## 概要
外部JSONファイルからデータを fetch で取得し、ドロップダウンリスト(<select>)の選択肢を動的に生成するフォームUIを実装してください。

## 要件
- ページ読み込み時に fetch でJSONファイルを取得し、配列データから <option> を動的に生成する
- JSONには2つのデータセット(dataset1 / dataset2)を定義し、切り替えボタンで使用するデータを変えられるようにする
- 初期表示は dataset1 のデータ
- 「決定」ボタン押下で選択中の value(JSONのキー)とテキストを結果エリアに表示する
- 未選択(先頭の「選択してください」)のまま決定を押した場合は警告を表示する

## JSONデータ仕様
ファイル名: data/data.json(HTMLと同じフォルダの data/ 配下に配置)

{
  "dataset1": {
    "label": "都道府県",
    "items": [
      { "key": "tokyo",     "name": "東京都" },
      { "key": "kanagawa",  "name": "神奈川県" },
      { "key": "osaka",     "name": "大阪府" },
      { "key": "aichi",     "name": "愛知県" },
      { "key": "fukuoka",   "name": "福岡県" },
      { "key": "hokkaido",  "name": "北海道" },
      { "key": "kyoto",     "name": "京都府" },
      { "key": "hiroshima", "name": "広島県" },
      { "key": "miyagi",    "name": "宮城県" },
      { "key": "okinawa",   "name": "沖縄県" }
    ]
  },
  "dataset2": {
    "label": "業種",
    "items": [
      { "key": "it",            "name": "IT・ソフトウェア" },
      { "key": "finance",       "name": "金融・保険" },
      { "key": "retail",        "name": "小売・流通" },
      { "key": "medical",       "name": "医療・福祉" },
      { "key": "education",     "name": "教育・研究" },
      { "key": "manufacturing", "name": "製造業" },
      { "key": "construction",  "name": "建設・不動産" },
      { "key": "food",          "name": "飲食・ホテル" },
      { "key": "media",         "name": "メディア・広告" },
      { "key": "government",    "name": "公務員・行政" }
    ]
  }
}

## 技術仕様
- HTML / CSS / バニラJavaScript で実装
- 外部ライブラリ:なし
- レスポンシブ対応:必要

## 動作詳細
fetch でJSONを取得後、items 配列をループして <option value="{key}">{name}</option> を生成し select に追加する。
データセット切り替え時は select 内の option(先頭の「選択してください」以外)を削除してから再生成する。
決定時は select.value でキーを、select.options[select.selectedIndex].text でテキストを取得して表示する。
fetch() は file:// では動作しないため、簡易サーバー(Live Server 等)が必要な旨をコメントに記載する。

## 出力形式
HTML・CSS・JavaScriptを分けて出力してください。
JSONファイル(data/data.json)も合わせて出力してください。
各ファイルは単独でコピー&ペーストして使えるよう記述してください。

Pattern 2 — グリッドリスト(動的生成・複数選択)

# グリッドリスト(動的生成・複数選択)作成依頼

## 概要
外部JSONファイルからデータを fetch で取得し、チェックボックスをグリッドレイアウトで動的生成するフォームUIを実装してください。

## 要件
- ページ読み込み時に fetch でJSONファイルを取得し、配列データからチェックボックスをグリッド状に動的生成する
- JSONには2つのデータセット(dataset1 / dataset2)を定義し、切り替えボタンで使用するデータを変えられるようにする
- 初期表示は dataset1 のデータ
- チェックボックスは横並び・折り返し(各アイテム 160px 固定幅)でグリッド状に配置する
- 「決定」ボタン押下でチェック中の全アイテムの value(JSONのキー)とテキストをリスト表示する
- 未チェック状態で決定を押した場合は「1つ以上選んでください」と警告を表示する

## JSONデータ仕様
ファイル名: data/data.json(HTMLと同じフォルダの data/ 配下に配置)

{
  "dataset1": {
    "label": "都道府県",
    "items": [
      { "key": "tokyo",     "name": "東京都" },
      { "key": "kanagawa",  "name": "神奈川県" },
      { "key": "osaka",     "name": "大阪府" },
      { "key": "aichi",     "name": "愛知県" },
      { "key": "fukuoka",   "name": "福岡県" },
      { "key": "hokkaido",  "name": "北海道" },
      { "key": "kyoto",     "name": "京都府" },
      { "key": "hiroshima", "name": "広島県" },
      { "key": "miyagi",    "name": "宮城県" },
      { "key": "okinawa",   "name": "沖縄県" }
    ]
  },
  "dataset2": {
    "label": "業種",
    "items": [
      { "key": "it",            "name": "IT・ソフトウェア" },
      { "key": "finance",       "name": "金融・保険" },
      { "key": "retail",        "name": "小売・流通" },
      { "key": "medical",       "name": "医療・福祉" },
      { "key": "education",     "name": "教育・研究" },
      { "key": "manufacturing", "name": "製造業" },
      { "key": "construction",  "name": "建設・不動産" },
      { "key": "food",          "name": "飲食・ホテル" },
      { "key": "media",         "name": "メディア・広告" },
      { "key": "government",    "name": "公務員・行政" }
    ]
  }
}

## 技術仕様
- HTML / CSS / バニラJavaScript で実装
- 外部ライブラリ:なし
- レスポンシブ対応:必要(スマホでは2列になるよう調整)

## 動作詳細
fetch でJSONを取得後、items 配列をループして <div class="fs3-grid-item"><label><input type="checkbox" value="{key}" data-name="{name}"> {name}</label></div> を生成しグリッドコンテナに追加する。
データセット切り替え時はグリッドの innerHTML をクリアしてから再生成する。
決定時は querySelectorAll('input[type="checkbox"]:checked') でチェック済みを取得し、cb.value(キー)と cb.dataset.name(表示名)を表示する。
fetch() は file:// では動作しないため、簡易サーバー(Live Server 等)が必要な旨をコメントに記載する。

## 出力形式
HTML・CSS・JavaScriptを分けて出力してください。
JSONファイル(data/data.json)も合わせて出力してください。
各ファイルは単独でコピー&ペーストして使えるよう記述してください。