Plain JS Cascading selects of unknown depth
Asked Answered
I

2

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>
Instinct answered 11/2, 2019 at 15:41 Comment(12)
This better be off as a drill-down menu, rather than select boxes. Something like: filamentgroup.com/examples/menus/ipod.htmlMetronymic
Sure, but that is not the spec. I am trying to help a friendInstinct
What do you need help with exactly? To create the dropdown elements or just to walk through your structure?Aboard
Create the relevant dropdowns based on each selectionInstinct
@Instinct do you want to dynamically create something like this (w3schools.com/bootstrap/…)Turgot
More like normal selectsInstinct
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)Barograph
The main issue is to recurse the object and identify the paths - not the CSS and such. Environment is plain JS and HTMLInstinct
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.Frendel
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.Instinct
do you have an example, how the result should look like?Light
The answer does a pretty good jobInstinct
V
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>
Violone answered 15/2, 2019 at 13:41 Comment(3)
"Doing this in React would be easier" ahem, assuming I would have any knowledge of react :OInstinct
What was changed to fix Edge?Instinct
Edge doesn't have the input event on selects, so just changed it to change instead. Tip: To see changes on post's, click the edited link below message.Violone
G
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>
Grain answered 20/2, 2019 at 21:46 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.