8

Wanted to make a generic cascading dropdown but am weak in recursion

The code is supposed to end up with

  • One select for items - clothes or gadgets - when a choice is made
    • One select with either Levis/Gucci or LG/Apple - when a choice is made
      • One select with either Levis jeans or jackets or Gucci shoes or dresses - when a choice is made
        • One select with Levis jeans sizes OR levis jacket sizes OR
        • One select with Gucci shoe sizes OR Gucci dress sizes

OR

      • One select with either LG TVs or phones or Apple Macbooks or iPhones - when a choice is made
        • One select with LG TV sizes OR LG Phone sizes OR
        • One select with Apple Macbook sizes OR Apple iPhone sizes

I lost my train of thoughts when I got to actually recurse - or perhaps filtering can be used?

I assume one could make a set of paths and then just show/hide depending on path

const selObject = {
  "-- Select Item --": {
    "Clothes": {
      "-- Select brands --": {
        "Levis": {
          "-- Select product --": {
            "Jeans": {
              "-- Select size --": [
                "38",
                "39",
                "40"
              ]
            },
            "Jackets": {
              "-- Select size --": [
                "41",
                "42",
                "43"
              ]
            }
          }
        }, // end Levis
        "Gucci": {
          "-- Select product --": {
            "Shoes": {
              "-- Select size --": [
                "45",
                "50",
                "55"
              ]
            },
            "Dresses": {
              "-- Select size --": [
                "8",
                "9",
                "10"
              ]
            }
          }
        } // end Gucci
      } // end brands  
    }, // End clothes
    "Gadgets": {
      "-- Select brands --": {
        "LG": {
          "-- Select product --": {
            "TVs": {
              "-- Select size --": [
                "38",
                "39",
                "40"
              ]
            },
            "Phones": {
              "-- Select size --": [
                "8",
                "9",
                "10"
              ]
            }
          }
        }, // end Levis
        "Apple": {
          "-- Select product --": {
            "Macbooks": {
              "-- Select size --": [
                "15",
                "17",
                "21"
              ]
            },
            "iPhones": {
              "-- Select size --": [
                "8",
                "9",
                "10"
              ]
            }
          }
        } // end Apple
      } // end brands
    } // end  Gadgets
  } // end items
} // end  

function createSel(obj) {
  Object.keys(obj).forEach(function(item) {
    if (typeof obj[item] == "object") {
      var list = obj[item];
      //console.log(item,typeof list);
      if (typeof list == "object") {
        if (list.length) {
          list.forEach(function(val) {
            console.log('<br/>'+val)
          })  
        }  
        else createSel(list)
      }
    } else {
      console.log("no", obj[item])
    }
  });
}
window.onload = function() {
  createSel(selObject)
}
<form name="myform" id="myForm">
  <div id="selContainer">
  </div>
</form>
mplungjan
  • 169,008
  • 28
  • 173
  • 236
  • This better be off as a drill-down menu, rather than select boxes. Something like: https://www.filamentgroup.com/examples/menus/ipod.html – jayarjo Feb 14 '19 at 06:49
  • Sure, but that is not the spec. I am trying to help a friend – mplungjan Feb 14 '19 at 07:22
  • What do you need help with exactly? To create the dropdown elements or just to walk through your structure? – Kaiido Feb 14 '19 at 08:07
  • Create the relevant dropdowns based on each selection – mplungjan Feb 14 '19 at 08:19
  • @mplungjan do you want to dynamically create something like this (https://www.w3schools.com/bootstrap/tryit.asp?filename=trybs_ref_js_dropdown_multilevel_css&stacked=h) – Maheer Ali Feb 14 '19 at 18:36
  • More like normal selects – mplungjan Feb 14 '19 at 19:38
  • 1
    Quick questions, is the environement Html? And are you in the obligation of doing it in javascript? Full css not an option? (with a tiny javascript function to build the menu from a json file or anything else) – Richard Feb 15 '19 at 13:10
  • The main issue is to recurse the object and identify the paths - not the CSS and such. Environment is plain JS and HTML – mplungjan Feb 15 '19 at 13:11
  • Are you married to the idea of having that exact data structure as the basis, or is that still flexible? I am wondering if it the best idea to have those `-- Select Item --` etc. always “one level up”? I am assuming that the first select is supposed to show the three options “-- Select Item --”, “Clothes”, and “Gadgets”, right? First as placeholder, others as the actual valid choices. I imagine it might be easier if you had those three on the same level in your data structure to begin with. – 04FS Feb 15 '19 at 13:21
  • Yeah, I guess that is a good idea. Again it is an exercise in a readable recursive traversing of a deeply nested mixed object that I am looking for. – mplungjan Feb 15 '19 at 13:23
  • do you have an example, how the result should look like? – Nina Scholz Feb 16 '19 at 15:51
  • 1
    The answer does a pretty good job – mplungjan Feb 16 '19 at 15:52

2 Answers2

4

Doing this in React would be easier,. But for a plain JS solution the below might be what your after.

Basically all I'm doing is using recursion to create the components, and attach the events.

const selObject = {
  "-- Select Item --": {
    "Clothes": {
      "-- Select brands --": {
        "Levis": {
          "-- Select product --": {
            "Jeans": {
              "-- Select size --": [
                "38",
                "39",
                "40"
              ]
            },
            "Jackets": {
              "-- Select size --": [
                "41",
                "42",
                "43"
              ]
            }
          }
        }, // end Levis
        "Gucci": {
          "-- Select product --": {
            "Shoes": {
              "-- Select size --": [
                "45",
                "50",
                "55"
              ]
            },
            "Dresses": {
              "-- Select size --": [
                "8",
                "9",
                "10"
              ]
            }
          }
        } // end Gucci
      } // end brands  
    }, // End clothes
    "Gadgets": {
      "-- Select brands --": {
        "LG": {
          "-- Select product --": {
            "TVs": {
              "-- Select size --": [
                "38",
                "39",
                "40"
              ]
            },
            "Phones": {
              "-- Select size --": [
                "8",
                "9",
                "10"
              ]
            }
          }
        }, // end Levis
        "Apple": {
          "-- Select product --": {
            "Macbooks": {
              "-- Select size --": [
                "15",
                "17",
                "21"
              ]
            },
            "iPhones": {
              "-- Select size --": [
                "8",
                "9",
                "10"
              ]
            }
          }
        } // end Apple
      } // end brands
    } // end  Gadgets
  } // end items
} // end  


function fillDropdown(target, obj) {
  const sel = document.createElement("select");
  const sub = document.createElement("div");
  if (typeof obj !== "object") {
    sub.innerHTML = "<p>Thank you for your selection</p>";
    target.appendChild(sub);
    return;
  }
  target.appendChild(sel);
  target.appendChild(sub);
  const [title, value] = Object.entries(obj)[0];
  //add our title option
  const option1 = document.createElement("option");
  option1.innerText = title;
  sel.appendChild(option1);
  //now add the sub items
  const items = Object.entries(value);
  items.forEach(([k, v]) => {
    const option = document.createElement('option');
    option.innerText = k;
    sel.appendChild(option);
  });
  sel.addEventListener("change", () => {
    sub.innerHTML = "";
    if (sel.selectedIndex > 0) {
      const i = items[sel.selectedIndex - 1];    
      fillDropdown(sub, i[1]);
    }
  }); 
}


window.onload = function() {
  //createSel(selObject);
  fillDropdown(
    document.querySelector('#selContainer'),
    selObject
  );
}
select {
  display: block;
  width: 100%;
  padding: 10px;
}
<form name="myform" id="myForm">
  <div id="selContainer">
  </div>
</form>
Keith
  • 22,005
  • 2
  • 27
  • 44
1

here's some other options you might want to consider :

using OptGroup :

const selObject = { "-- Select Item --": { Clothes: { "-- Select brands --": { Levis: { "-- Select product --": { Jeans: { "-- Select size --": ["38", "39", "40"] }, Jackets: { "-- Select size --": ["41", "42", "43"] } } }, Gucci: { "-- Select product --": { Shoes: { "-- Select size --": ["45", "50", "55"] }, Dresses: { "-- Select size --": ["8", "9", "10"] } } } } }, Gadgets: { "-- Select brands --": { LG: { "-- Select product --": { TVs: { "-- Select size --": ["38", "39", "40"] }, Phones: { "-- Select size --": ["8", "9", "10"] } } }, Apple: { "-- Select product --": { Macbooks: { "-- Select size --": ["15", "17", "21"] }, iPhones: { "-- Select size --": ["8", "9", "10"] } } } } } } };

const generateDropDown = (obj, indent) => {
  const spaces = Array(indent).fill('&nbsp;').join('');
  
  if (Array.isArray(Object.values(obj)[0])) {
    return Object.values(obj)[0].map(e => "<option>" + spaces + e + "</option>").join('');
  } else {
    return Object.values(obj).map(brand => {
      return Object.keys(brand).map(product => {
        //?
        return `<optgroup label="${spaces + product}"> ${generateDropDown(brand[product], indent + 4)} </optgroup>`;
      }).join('');
    });
  }
};


const list = generateDropDown(selObject, 0).join(' ');

document.querySelector('#dropDown').innerHTML = list;
<select id="dropDown">

</select>

using ul : ( more flexible to styling )

const selObject = { "-- Select Item --": { Clothes: { "-- Select brands --": { Levis: { "-- Select product --": { Jeans: { "-- Select size --": ["38", "39", "40"] }, Jackets: { "-- Select size --": ["41", "42", "43"] } } }, Gucci: { "-- Select product --": { Shoes: { "-- Select size --": ["45", "50", "55"] }, Dresses: { "-- Select size --": ["8", "9", "10"] } } } } }, Gadgets: { "-- Select brands --": { LG: { "-- Select product --": { TVs: { "-- Select size --": ["38", "39", "40"] }, Phones: { "-- Select size --": ["8", "9", "10"] } } }, Apple: { "-- Select product --": { Macbooks: { "-- Select size --": ["15", "17", "21"] }, iPhones: { "-- Select size --": ["8", "9", "10"] } } } } } } };

const generateDropDown = (obj, indent) => {  
  const values = Object.values(obj);
  if (Array.isArray(values[0])) {
    return values[0].map(e => `<li class="child">${e} </li>`).join(' ');
  } else {
    return values.map(brand => {
      return Object.keys(brand).map(product => {        
        return `<ul class="parent"> <li class="title">${product}</li> ${generateDropDown(brand[product], indent + 2)} </ul>`;
      }).join(' ');
    });
  }
};

const list = generateDropDown(selObject, 0).join(' ');

document.querySelector('#dropDown').innerHTML = list;

[...document.querySelectorAll('ul,li')].forEach(e => { 
 e.addEventListener('click', ev => {
   ev.cancelBubble = true;    
    ev.target.classList.toggle('open');
   // console.log(ev.target.innerText)
    // do some stuff when the element is clicked.    
  })
})
ul{
  padding: 0;
  margin: 0;
  list-style-type: none;
}

.parent, .child{
  padding-left: 15px;  
  display: none;
  cursor: pointer;
}

#dropDown > .parent {
  padding-left: 0;
  display: block;
  
}

.open ~ .parent{
  opacity: 1;
  display: block;
}

.open ~ .child{
  opacity: 1;
  display: block;
}

.title{
  font-weight: bold;
}

.title.open{
  color: red;
}
<div id="dropDown">

</div>
Taki
  • 17,320
  • 4
  • 26
  • 47