jQuery / Criar campos select dependentes

Campos select dependentes mudam as opções do segundo conforme a primeira seleção. Escute change do pai, esvazie o filho e acrescente os dados correspondentes. Reinicie também o valor antigo para não manter um item de outro grupo.

Definir dados e funções

O exemplo guarda Group A e B e seus arrays localmente, sem servidor nem plugin extra. O valor a escolhe seu array. Separe label visível e value enviado para que alterações de texto preservem o identificador estável.

Cada controle tem label associado e id único; name identifica os dados do formulário. O elemento select usa required para valor vazio, e o filho fica disabled sem itens. Controles desabilitados são omitidos, portanto o servidor também precisa conferir valores ausentes e combinações inválidas.

A opção vazia é orientação, não item real. Dê identificadores não vazios às opções reais para distinguir uma escolha deliberada.

Exemplo completo de listas relacionadas

Salve e abra dependent-select-demo.html em UTF-8. A internet carrega jQuery completo 4.0.0; não requer servidor nem compilação. Windows Edge com 4.0.0 e 3.7.1 verificou teclado, restauração, grupos vazios/desconhecidos, inspeção de pares e mudanças repetidas.

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

No início não há seleção, existem zero opções reais além da orientação e o filho está desabilitado.

group=(none)
item=(none)
options=0; disabled=true
changes=0
  1. Group A mostra Choose an item, A-1 e A < B & C: dois itens reais.
  2. A-1 e Inspect selection exibem Valid pair: {"group":"a","item":"a1"}. Nada é enviado para fora.
  3. Group B troca para B-1 e B-2 e restaura a orientação vazia. a1 não deve permanecer.
  4. Sem escolher o filho, required bloqueia antes de submit. A mensagem depende do navegador e idioma.
  5. Group without items desabilita o filho com No available items. A inspeção pede um par disponível.
  6. Reset restaura o início. Confira também a seleção com setas do teclado.

changes conta execuções dos manipuladores change, não opções, cliques ou pessoas. Inicialização e Reset deixam zero; um evento programado também conta.

Reconstrução e eventos

.on("change", ...) conecta os controles existentes. O valor pai busca um array Map; chave ausente resulta em array vazio. Depois de esvaziar e acrescentar orientação e itens, .val("") remove a escolha antiga. Alternâncias A/B não acumulam opções.

new Option(label, value) cria texto sem concatenar HTML. A < B & C permanece literal. O valor lido é a1 ou a2, não necessariamente o texto visível.

Definir .val() não dispara change automaticamente. Para outra função escolher Group B, execute isto depois de inicializar e reconstrua o filho pelo mesmo caminho.

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

rebuildItem é privado dentro de ready. Use eventos ou projete uma API pública adequada. Se o select inteiro for substituído, reveja eventos diretos; delegar de um pai estável pode ajudar.

Reset chama form.reset() e recalcula o filho. A restauração nativa não reconstrói opções criadas por JavaScript. O id do botão é restore para não ocultar o método do formulário com um controle chamado reset.

Validar pares e ampliar

O manipulador submit impede o envio normal e confere associação ao array atual. required trata vazios habilitados, mas um filho disabled de grupo vazio fica fora da validação nativa. Formulário HTML válido e par válido para o negócio são condições diferentes.

FormData usa valores atuais dos controles com name e omite o filho desabilitado. É uma demonstração do navegador. Um servidor real precisa validar independentemente a relação permitida usando seus dados.

Com três níveis, mudar o primeiro apaga listas e valores do segundo e terceiro. O terceiro só é preenchido após um segundo válido. Para restaurar valores: dados pai, seleção pai, geração filha e valor filho válido.

Carregamento remoto exige lógica extra: limpar valores durante espera, distinguir carregando/erro/vazio e cancelar ou identificar solicitações para uma resposta A lenta não sobrescrever B. Aqui não há rede nem controle de concorrência implementados.

Se não mudar, confira IDs e valores reais, ordem e Console. Para valor antigo persistente, examine a sequência de opções e .val(). Revise labels, interface móvel nativa e textos longos sem forçar foco no próximo campo.