Hierarchical dependent dropdowns allow subsequent dropdown filters to exclude any irrelevant data based on other selections. For example, you may have a series of filters for state and city. Once the user selects a state, you want the options for the city dropdown to exclude any cities not in that state. This code was originally developed by the Get \_\_it Done YouTube channel in the playlist [Dependent Dropdowns - Unlimited Levels](https://youtube.com/playlist?list=PLRmEk9smitaUDco5Y_bYClnFNCHx1jmWY&si=fzFzipQ6aEDyhEIw). I've adapted it slightly, including to use the keyword "All" to include all values at that filter. I've adapted the code and used it in projects including the [USAID/Guatemala Data Catalog](https://github.com/eriktuck/gtm-apps/tree/main/apps/data-catalog) and the [Environmental Incentives Look Book](https://script.google.com/home/projects/1wV9OqFVBBvQSPizbfo392BIckRHPH9OxKtQTPY0GzXqa1YRkwFLLEmB5/edit). I use `.includes()` to allow the most inclusive filtering, you could instead ask for exact match in the `filterData` method. To use this code, first copy the `DDropdown` class into your project. Note that this class relies on global variables `DATA`, `HEADERS`, and `DDCOLS`, which are specified in subsequent code blocks below. ```JavaScript class DDropDown{ // https://www.youtube.com/playlist?list=PLRmEk9smitaUDco5Y_bYClnFNCHx1jmWY constructor(data){ this.data = data; this.targets=[]; } filterData(filtersAsArray){ // Use 'All' as option in filter to get All values const filteredData = this.data.filter(r => { return filtersAsArray.every((item, i) => { return item === 'All' || item === '' ? true : r[i].includes(item) }) }); return filteredData } getUniqueValues(dataAsArray, index){ const uniqueOptions = new Set(); dataAsArray.forEach(r => { return r[index].split(',').forEach(item => { return item !== '' ? uniqueOptions.add(item.trim()) : {} }) }); return [...uniqueOptions].sort(); } populateDropDown(el, listAsArray){ const currentValue = el.value; // clear options el.innerHTML = ""; // add All as option const allOption = document.createElement('option'); allOption.textContent='All'; el.appendChild(allOption); // add dependent options listAsArray.forEach(item => { const option = document.createElement('option'); option.textContent = item; el.appendChild(option); }); // set value el.value = currentValue === '' ? 'All' : currentValue; } createPopulateDropDownFunction(el, elsDependsOn, elIndex){ return () => { var elsDependsOnValues = elsDependsOn.length === 0 ? null : elsDependsOn.map(depEl => depEl.value); elsDependsOnValues.splice(elIndex, 0, ''); const dataToUse = elsDependsOn.length === 0 ? this.data : this.filterData(elsDependsOnValues); const listToUse = this.getUniqueValues(dataToUse, elIndex); this.populateDropDown(el, listToUse); } } applyFilters() { return () => { var filterEls = document.querySelectorAll(".filters"); var filtersAsArray = []; filterEls.forEach(el => filtersAsArray.push(el.value)); var filterIndices = DDCOLS.map(col => HEADERS.indexOf(col)); var filteredData = DATA.filter(row => { return filtersAsArray.every((item, i) => { return item === 'All' || item === '' ? true : row[filterIndices[i]].includes(item); }); }); setFilterData(filteredData); } } add(options){ /* options = {target: "level2", dependsOn: ["level1"], elIndex: 2, elProp: 'prop1'} */ const el = document.getElementById(options.target); const elsDependsOn = options.dependsOn.length === 0 ? [] : options.dependsOn.map(id => { return document.getElementById(id) }); const elsAsArray = elsDependsOn.concat(el); const initFunction = this.createPopulateDropDownFunction(el, elsDependsOn, options.elIndex); const changeFunction = getResults; const targetObject = { el: el, elsDependsOn: elsDependsOn, func: initFunction, elProp: options.elProp }; el.addEventListener('change', changeFunction); targetObject.elsDependsOn.forEach(depEl => depEl.addEventListener('change', initFunction)); this.targets.push(targetObject); return this; } initialize(){ this.targets.forEach(t => t.func()); return this; } eazyDropDown(arrayOfIds, arrayOfProps){ // TODO could be single dictionary as input {el: {target: elId, elProp: 'Name'}} arrayOfIds.forEach((item, i) => { const dependsOn = arrayOfIds.filter(val => val !== item); const option = { target: item, dependsOn: dependsOn, elIndex: i, elProp: arrayOfProps[i]}; this.add(option); }); this.initialize(); } } ``` To initialize the Dropdowns I write a `createDropDowns()` function to be called by the function `initializeApp()` after the DOM content is loaded. The example below is boilerplate for a Google Apps Script [[web app]]. ```JavaScript let HEADERS; let DATA; let DDCOLS = ['State', 'City']; // order by display, should exactly match columns in spreadsheet let ELIDS = ['state', 'city'] // match in index.html // set data function setData(dataReturned){ HEADERS = dataReturned.shift(); DATA = dataReturned.slice(); }; // create DropDown menus function createDropDowns(){ var ddColIdxs = DDCOLS.map(col => HEADERS.indexOf(col)); var ddData = DATA.map(row => { return row.filter((_, idx) => ddColIdxs.includes(idx)) }); var dd = new DDropDown(ddData); dd.eazyDropDown( ELIDS, DDCOLS ); } // populate UI function populateCards(data) { if(data){ // Do something with filtered data }; } // initialize app function initializeApp(dataReturned){ setData(dataReturned); createDropDowns(); populateCards(DATA); // Populate UI initially } // Event listener document.addEventListener('DOMContentLoaded', function(){ // Get data google.script.run.withSuccessHandler(initializeApp).getData(); }); ``` In the above code, `getData()` is a simple function in `Code.gs` for getting data as an array; customize for your application. In my HTML I'll use [[base/Web/Bootstrap]] to include the `<select>` elements within a form group that will function as dropdown filters. ```HTML <!-- Filters --> <div id="filterContainer" class="rounded bg-light"> <div class="d-grid gap-2"> <button type="button" class="btn btn-block btn-light" data-bs-toggle="collapse" data-bs-target="#filters">Filter Products</button> </div> <div id="filters" class="collapse show"> <form> <div class="form-group"> <label for="state">State</label> <select id="state" class="form-select filters" aria-label="Select a State"> </select> </div> <div class="form-group"> <label for="city">City</label> <select id="city" class="form-select filters" aria-label="Select a City"> </select> </div> </form> </div> ``` Finally, you'll just need to include a `<div>` to write out the results.