Subcategories aren't populating using fetch

I'm trying to create <optgroup>s that have subcategories pulled from an API. Currently, only the <optgroup>s are populating, but not the subcategories.

let select = document.querySelector("#category");
const categoriesURL = "INSERT_URL"
  
let str = "";

fetch(categoriesURL)
  .then(response => response.json())
  .then(data => {
    data.forEach(category => {
      let categoryTitle = category.title;
      str += `<optgroup label=${categoryTitle}></optgroup>`
      category.subcategories.forEach(sub => {
        let subcategories_id = sub.subcategories_id;
        let subcategoriesURL = `INSERT_URL/${subcategories_id}`;
        fetch(subcategoriesURL)
          .then(response => response.json())
          .then(subData => {
          str += `<option value=${sub.subcategories_id}>` + subData.title + "</option>";
        })
      })
  })
  select.innerHTML = "<option disabled selected>Select a category</option>" + str;
});

Solution 1:

When defining your optgroup in the string, you immediately open and close the optgroup, effectively putting every option after the optgroup.

str += `<optgroup label=${categoryTitle}></optgroup>`

Leave the optgroup open in the aforementioned string.

However, the hard part is closing the string after all the fetch requests on the subcategories are complete. We can accomplish this with Promise.all and Array#map.

Loop over all the subcategories with map and return Promise that fetch returns in the loop. When all the fetch requests are done the code will continue to the then block that follows the Promise.all function. In that block you will have the ids and titles of all your subcategories collected in an array. Loop over array to append to your string.

After the loop, close the optgroup element.

fetch(categoriesURL)
  .then(response => response.json())
  .then(data => {
    data.forEach(category => {
      let categoryTitle = category.title;
      str += `<optgroup label=${categoryTitle}>`;

      Promise.all(category.subcategories.map(sub => {
        let subcategories_id = sub.subcategories_id;
        let subcategoriesURL = `INSERT_URL/${subcategories_id}`;

        return fetch(subcategoriesURL)
          .then(response => response.json())
          .then(({ title }) => ({
            title,
            id: subcategories_id
          }))
      })).then(subData => {
        subData.forEach(({ id, title }) => {
          str += `<option value=${id}>${title}</option>`;
        })
        
        str += '</optgroup>';
      });
    })

    select.innerHTML = "<option disabled selected>Select a category</option>" + str;
  });

Though, overall this a very expensive script as it will create a lot of requests. If you have any say over the backend, then I'd advice that you send the all the data back in a single request and create your list based on that result.