jQuery / Créer des listes select dépendantes

Des listes select dépendantes modifient les options de la seconde selon la première sélection. Écoutez change du parent, videz l’enfant puis ajoutez les données correspondantes. Effacez aussi son ancienne valeur pour éviter de conserver un élément du groupe précédent.

Définir données et rôles

L’exemple garde localement Group A et B avec leurs tableaux d’éléments, sans serveur ni plugin supplémentaire. La valeur a choisit son tableau. Séparez label visible et value transmise pour qu’un changement de texte ne change pas l’identifiant stable.

Chaque contrôle a un label lié et un id unique ; name identifie les données transmises. L’élément select utilise required contre une valeur vide. Sans élément, l’enfant devient disabled. Un contrôle désactivé est omis des données, donc valeurs absentes et combinaisons incorrectes demandent aussi une vérification côté serveur.

L’option vide est une consigne, pas un élément réel. Donnez aux vrais éléments des identifiants non vides pour distinguer une sélection volontaire.

Exemple complet de listes liées

Enregistrez et ouvrez dependent-select-demo.html en UTF-8. Internet charge jQuery complet 4.0.0 ; aucun serveur ni compilation. Les tests Windows Edge avec 4.0.0 et 3.7.1 ont couvert clavier, réinitialisation, groupes vides/inconnus, inspection de paire et changements répétés.

<!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>

Au départ, aucun choix, zéro option réelle au-delà de la consigne et enfant désactivé.

group=(none)
item=(none)
options=0; disabled=true
changes=0
  1. Group A affiche Choose an item, A-1 et A < B & C : deux éléments réels.
  2. A-1 puis Inspect selection donne Valid pair: {"group":"a","item":"a1"}. Rien n’est envoyé à l’extérieur.
  3. Group B remplace par B-1 et B-2 et remet l’enfant à la consigne vide. a1 ne doit pas rester.
  4. Sans choix enfant, required bloque avant submit. Le message varie selon navigateur et langue.
  5. Group without items désactive l’enfant avec No available items. L’inspection réclame une paire disponible.
  6. Reset restaure le départ. Vérifiez aussi la sélection avec les flèches du clavier.

changes compte l’exécution des gestionnaires change, pas options, clics ou personnes. Départ et Reset donnent zéro ; un événement programmé compte aussi.

Reconstruction et événements

.on("change", ...) lie les deux contrôles existants. La valeur parent recherche un tableau Map ; une clé inconnue donne un tableau vide. Après effacement et insertion de la consigne et des éléments, .val("") retire le choix précédent. Les allers-retours A/B n’accumulent donc pas d’options.

new Option(label, value) traite le libellé comme texte, sans concaténation HTML. A < B & C reste littéral. La valeur lue est a1 ou a2, pas nécessairement le texte visible.

Le setter .val() ne déclenche pas change automatiquement. Pour faire choisir Group B par une autre fonctionnalité, exécutez ceci après initialisation afin de reconstruire l’enfant par le même chemin.

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

rebuildItem est privé dans le callback ready. Utilisez des événements ou concevez une API publique pour l’autre code. Si le select lui-même est remplacé, ses gestionnaires directs sont à revoir ; une délégation depuis un parent stable peut convenir.

Reset appelle form.reset() puis recalcule l’enfant. La remise à zéro native ne reconstruit pas les options JavaScript. L’id du bouton est restore afin de ne pas masquer la méthode du formulaire avec un contrôle nommé reset.

Valider les paires et étendre

Le gestionnaire submit empêche l’envoi normal et vérifie l’appartenance au tableau courant. required traite les choix vides actifs, mais un enfant disabled d’un groupe vide échappe à la validation native. Validité HTML et paire métier valide sont distinctes.

FormData utilise les valeurs courantes des contrôles nommés et omet l’enfant désactivé. C’est une démonstration dans le navigateur. Un vrai serveur doit vérifier indépendamment la relation autorisée avec ses propres données.

À trois niveaux, changer le premier efface les deuxième et troisième listes et valeurs. Le troisième se remplit après un deuxième choix valide. Pour restaurer des valeurs enregistrées : données parent, choix parent, génération enfant, puis choix enfant valide.

Le chargement distant demande une logique supplémentaire : vider les anciennes valeurs pendant l’attente, distinguer chargement/échec/vide, annuler ou identifier les requêtes pour qu’une réponse A tardive n’écrase pas B. Aucun réseau ni traitement de concurrence n’est implémenté ici.

Sans changement, contrôlez ID et valeurs réels, ordre et Console. Pour une ancienne valeur persistante, examinez l’ordre de création et .val(). Vérifiez labels, interface native mobile et longs textes sans forcer le focus sur le champ suivant.