Cascading Selects

by schinckel

HTML

<p>
 This will limit the available choices in the second SELECT based upon the
 selection in the first SELECT.
</p>

<p>
  Bonus: if you have a selection in the second SELECT, and then change the first
  SELECT, it will "remember" that selection if you return the first SELECT to the
  previous value.
</p>


<select name="x">
<option value="">Please select...</option>
<option value="1">One</option>
<option value="2">Two</option>
<option value="3">Three</option>
</select>

<br>

<select name="y" id="">
  <option value=""></option>
  <option value="A" data-x="1">A</option>
  <option value="B" data-x="1">B</option>
  <option value="C" data-x="2">C</option>
  <option value="D" data-x="1">D</option>
</select>

JavaScript

function IDENTITY(x) { return x; }

  function Related(sourceElement, targetElement) {
    // Given a sourceElement and targetElement, filter the available options in the targetElement
    // according to the selection in the sourceElement.
    if (!(this instanceof Related)) return new Related(sourceElement, targetElement);

    var allTargetOptions = Array.apply(null, targetElement.options).map(IDENTITY);
    var previousSelections = {};

    if (sourceElement.value) {
      previousSelections[sourceElement.value] = targetElement.value;
    }

    function filterTargetOptions() {
      var lastValue = targetElement.value;
      targetElement.innerHTML = '';
      targetElement.disabled = true;
      allTargetOptions.forEach(function(option) {
        if (!option.value || option.dataset[sourceElement.name] == sourceElement.value) {
          targetElement.options.add(option);
          if (option.value) {
            targetElement.removeAttribute('disabled');
          }
        }
      });
      targetElement.value = "";
      if (previousSelections[sourceElement.value]) {
        targetElement.value = previousSelections[sourceElement.value];
        if (lastValue != targetElement.value) {
          setTimeout(function() {
            targetElement.dispatchEvent(new Event('change', {bubbles: true}));
          }, 0);
        }
      }
      targetElement.dispatchEvent(new Event('options-change', {bubbles: true}));
    }

    filterTargetOptions();

    sourceElement.addEventListener('change', filterTargetOptions);
    targetElement.addEventListener('change', function() {
      previousSelections[sourceElement.value] = targetElement.value;
    });
  }


new Related(
	document.querySelector('[name="x"]'),
  document.querySelector('[name="y"]')
);