jQuery / 連動するselectを作る方法

連動するselectは、先に選んだ値に応じて次のselectのoptionを変える構成です。親のchangeを検知し、子を空にして対応データを入れ直します。以前の子の値も解除し、前のグループの項目が残らないようにします。

データと役割を決める

Group A・Bと各項目をローカル配列に持ち、サーバーや追加プラグインは使いません。親valueがaなら対応配列を読みます。表示labelと送信用valueを分けると、文言変更でも識別子を保てます。

各要素に対応するlabel、一意のid、送信用nameを付けます。select要素のrequiredは空値の検査に使い、項目がなければ子をdisabledにします。無効な要素は送信データから除かれるため、サーバー側では欠落と不正な組合せも確認します。

空のoptionは案内で実項目ではありません。実データには空でない識別子を使い、意図した選択と区別します。

一覧を連動させる完全な例

UTF-8のdependent-select-demo.htmlとして保存し開きます。ネットから完全版jQuery4.0.0を読み、サーバーやビルドは不要です。Windows Edgeで4.0.0・3.7.1それぞれのキーボード、初期化、空・未知グループ、組合せ確認と反復切替を検証しました。

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Dependent select controls</title>
  <script src="https://code.jquery.com/jquery-4.0.0.min.js"></script>
  <style>
    body { font: 16px/1.6 sans-serif; max-width: 720px; margin: 20px; }
    label { display: block; margin-top: 12px; }
    select, button { font: inherit; max-width: 100%; margin: 6px 0; }
    pre { white-space: pre-wrap; overflow-wrap: anywhere; }
  </style>
</head>
<body>
  <form id="chooser">
    <label for="group">Group</label>
    <select id="group" name="group" required>
      <option value="">Choose a group</option>
      <option value="a">Group A</option>
      <option value="b">Group B</option>
      <option value="empty">Group without items</option>
    </select>
    <label for="item">Item</label>
    <select id="item" name="item" required disabled><option value="">Choose a group first</option></select>
    <button type="submit">Inspect selection</button>
    <button type="button" id="restore">Reset</button>
  </form>
  <pre id="result" aria-live="polite"></pre>
  <p id="submission" role="status">Nothing inspected.</p>
  <script>
    jQuery(function ($) {
      const choices = new Map([
        ["a", [{value: "a1", label: "A-1"}, {value: "a2", label: "A < B & C"}]],
        ["b", [{value: "b1", label: "B-1"}, {value: "b2", label: "B-2"}]],
        ["empty", []]
      ]);
      const $group = $("#group"), $item = $("#item"), form = document.querySelector("#chooser");
      let changes = 0;
      function render() {
        $("#result").text("group=" + ($group.val() || "(none)") + "\nitem=" + ($item.val() || "(none)") +
          "\noptions=" + Math.max(0, $item[0].options.length - 1) + "; disabled=" + $item.prop("disabled") + "\nchanges=" + changes);
      }
      function rebuildItem() {
        const items = choices.get($group.val()) || [];
        const prompt = items.length ? "Choose an item" : ($group.val() ? "No available items" : "Choose a group first");
        $item.empty().append(new Option(prompt, ""));
        for (const item of items) $item.append(new Option(item.label, item.value));
        $item.val("").prop("disabled", items.length === 0);
        $("#submission").text("Nothing inspected."); render();
      }
      $group.on("change", function () { changes++; rebuildItem(); });
      $item.on("change", function () { changes++; $("#submission").text("Nothing inspected."); render(); });
      $(form).on("submit", function (event) {
        event.preventDefault();
        const items = choices.get($group.val()) || [];
        if (!items.some(item => item.value === $item.val())) {
          $("#submission").text("Choose a group with an available item."); return;
        }
        $("#submission").text("Valid pair: " + JSON.stringify(Object.fromEntries(new FormData(form))));
      });
      $("#restore").on("click", function () { form.reset(); changes = 0; rebuildItem(); });
      rebuildItem();
    });
  </script>
</body>
</html>

初期値は両方未選択、案内以外の項目0、子は無効です。

group=(none)
item=(none)
options=0; disabled=true
changes=0
  1. Group AでChoose an item、A-1、A < B & Cが出ます。実項目は二つです。
  2. A-1を選びInspect selectionでValid pair: {"group":"a","item":"a1"}となります。外部送信はしません。
  3. Group Bへ変えるとB-1・B-2となり、子は空案内へ戻ります。a1は残りません。
  4. 子を選ばず確認するとrequiredがsubmitイベントより先に止めます。案内文言はブラウザーと言語によります。
  5. Group without itemsでは子が無効となりNo available itemsです。確認時は有効な組合せを促します。
  6. Resetで初期状態へ戻し、方向キーでも同じ連動を確認します。

changesはchangeハンドラーの実行回数です。項目数・クリック数・利用人数ではありません。初期化とResetは0で、コードによるイベントも増加対象です。

再生成とchangeを理解する

.on("change", ...)は既存の二要素へ接続します。親の値からMap配列を探し、未知キーなら空配列にします。子を空にして案内と項目を追加し、.val("")で古い値を外します。A/Bを反復してもoptionは累積しません。

new Option(label, value)はHTML連結ではなく文字として作るため、A < B & Cもそのまま表示します。選択後の値は表示文言でなくa1・a2などの識別子です。

.val()の設定だけではchangeは自動発生しません。別機能からGroup Bを選ぶ場合、初期化後に次を実行して同じ経路で子も作り直します。

$("#group").val("b").trigger("change");

rebuildItemはready内部の非公開関数です。他のコードはイベントで連携するか公開APIを設計します。select自体を置換すると直接接続イベントも再検討が必要で、残る親からの委譲も選択肢です。

Resetはform.reset()後に子を再計算します。標準resetだけでJavaScript生成のoptionは再構築されません。ボタンidはrestoreとし、resetという名前でフォームのメソッドを隠す衝突を避けました。

組合せ検査と拡張

submitで通常送信を止め、現在グループの配列に子が含まれるか確認します。requiredは有効要素の空値には働きますが、空グループで子がdisabledなら標準検証から除外されます。HTML上の有効と業務上の有効な組合せは別です。

FormDataはname付き要素の現在valueを使い、disabledな子は省きます。これはブラウザー内の実習です。実サーバーは自身のデータで親子関係を再検証します。

三段階なら最初の変更で二番目と三番目の一覧・値を解除し、有効な二番目の選択後に三番目を作ります。保存値の復帰は親データ、親選択、子生成、有効な子選択の順です。

非同期取得では待機中に古い値を消し、読込・失敗・空を分けます。遅いA応答が新しいB一覧を上書きしないよう取消や要求識別が必要です。この例に通信や競合処理はありません。

変わらなければ実id・value・読込順・Consoleを確認します。古い値は生成と.val()の順序を点検します。label、実機の選択UI、長い文言も確認し、次の入力へ勝手にフォーカスを移しません。