Cascading Selects: tree data

Cascading selects, where only the leaf node will be sent as the actual form value.

by schinckel

HTML

<form>
  <!-- 
    This element shows that we do indeed include all items in the form
    with the same name.
  -->
  <fieldset>
    <label for="text-category">Extra value:</label>
    <input type="text" name="category" value="xxx" id="text-category">
  </fieldset>
  
  <fieldset>
    <label for="category">
      Category:
    </label>
    <select name="category" id="category">
      <option value=""></option>
      <option value="1">One</option>
      <option value="2">Two</option>
      <option value="3">Three</option>
    </select>
  </fieldset>
</form>

<pre id="category-value"></pre>

CSS

form {
  min-height: 200px;
}
fieldset {
  border: none;
}

label {
  min-width: 120px;
  display: inline-block;
}
select {
  display: block;
  margin-left: 124px;
  min-width: 80px;
}

select:first-of-type {
  display: inline-block;
  margin-left: 0;
}

pre {
  position: absolute;
  top: 0;
  right: 10px;
  border: 1px solid silver;
  background: rgba(0, 0, 0, 0.1);
  min-width: 200px;
  min-height: 42px;
  padding: 8px;
}

JavaScript

function fakeResponse(value) {
  switch (Number(value)) {
    case 1:
      return [
        {'value': 4, 'label': 'Four'},
        {'value': 5, 'label': 'Five'},
      ];
    case 2:
      return [];
    case 3:
      return [
        {'value': 6, 'label': 'Six'},
        {'value': 7, 'label': 'Seven'},
      ];
    case 5:
			return [
        {'value': 8, 'label': 'Eight'},
        {'value': 9, 'label': 'Nine'},
        {'value': 10, 'label': 'Ten'},
        {'value': 11, 'label': 'Eleven'},
      ];
    case 10:
    	return [
        {'value': 12, 'label': 'Twelve'},
        {'value': 13, 'label': 'Thirteen'},
      ]
  }
}


function Cascade(base_element, url) {
  const name = base_element.name;

  function removeChildren(element) {
    while (element.nextElementSibling) {
      element.nextElementSibling.remove();
    }
  }

  function setChildChoices(element, choices) {
	  if (!choices || !choices.length) {
    	return removeChildren(element);
    }
    if (element.nextElementSibling) {
      // First, remove all subesquent nodes.
      removeChildren(element.nextElementSibling);
    } else {
      // create the new node.
      element.parentElement.appendChild(element.cloneNode(true));
      element.nextElementSibling.addEventListener('change', handleChange);
    }
    // Clear the name, since we don't want to send it unless it has a value.
    let child = element.nextElementSibling;
    child.removeAttribute('name');
    let empty = Array.prototype.filter.call(child.options, x => x.value == '');
    child.innerHTML = '<option></option>';
    child.append(...choices.map(x => new Option(x.label, x.value)));
  }

  function handleChange(change) {
    if (change.target.value) {
      if (change.target.previousElementSibling) {
        change.target.previousElementSibling.removeAttribute('name');
      }
      change.target.setAttribute('name', name);
      // Perform an ajax request here, and see if we need to add extra values.
      // fetch(url).then(r =>...