Blog

Dynamic dropdowns

22 March 2018 No comments

Ever had to create a form that contained two dropdowns that were dependent on each other? For example, select a brand first, then a model of that brand. The second <select> element should then only contain options that are relevant to first selection. There are numerous ways of doing this, but here’s my preferred method.

First of all, you need to have your data structured properly. You’re looking at a parent-child relationship (Brands and Models), which I prefer to store as categories, where the relationship between the entities is clear immediately. In this example, I’m assuming ExpressionEngine categories (which would work well in combination with Low Search), but feel free to use any setup or CMS you find appropriate; the effect is exactly the same, as the solution is front-end only.

To make sure your options are accessible even if there’s no JavaScript available, we’re going to create a single <select> element that contains both Brands and Models, where we’re going to reference the Brand ID (parent ID) in the Model option using a data-attribute. The generated result should be something along these lines:

<select name="category[]" class="parent-child">
    <option value="">--</option>
    <option value="1">Brand A</option>
    <option value="2" data-parent-id="1">Model A1</option>
    <option value="3" data-parent-id="1">Model A2</option>
    <option value="4" data-parent-id="1">Model A3</option>
    <option value="5">Brand B</option>
    <option value="6" data-parent-id="5">Model B1</option>
    <option value="7" data-parent-id="5">Model B2</option>
    <option value="8" data-parent-id="5">Model B3</option>
    ...
</select>

The general idea here is to:

  1. Duplicate the <select> element, without child nodes;
  2. Remove and store all child options by their parent ID;
  3. Add an event on the change listener on the first dropdown that puts all appropriate <option>s in the new dropdown.

In vanilla JavaScript, this translates to:

(function(){
    let select   = document.querySelector('select.parent-child');
    let children = select.querySelectorAll('[data-parent-id]');
    let target   = select.cloneNode();
    let map      = {};

    // Add target to DOM
    select.parentNode.appendChild(target);

    // Remove children, store in map
    children.forEach(function(child){
        let parentId = child.dataset.parentId;
        select.removeChild(child);
        if (!Array.isArray(map[parentId])) map[parentId] = [];
        map[parentId].push(child);
    });

    // Populate target on select change
    select.addEventListener('change', function(){
        let options = map[select.value] || [];
        target.innerHTML = '';
        for (let option of options) target.appendChild(option);
        target.selectedIndex = 0;
    });
})();

And done! No Ajax calls needed, and even if there’s no JavaScript available, the user can make an appropriate selection. Check out this JSFiddle for a working example.

Commenting is not available in this channel entry.