javascriptnested-lists

Recursive nested ul ordered by depth


I am creating a nested list from an array generated from an hierarchical table of categories, with subcategories having a parent ID of the parent category ID. I am trying to display the list in a format like folders and files, where list items with more items in them are grouped together like folders followed by those that don't, let's call them files.

I have managed to create the list but each set of categories and subcategories are displaying in alphabetical order as opposed to the desired format outlined below. I have tried to achieve the desired format inside the recursive function as well as a separate function targeting these "folders" after the list has been generated.

The desired outcome would look like this:

<ul id='myList' class="folder">
  <li data-id=0>
    <ul class="folder">
      <li data-id=1>catA
        <ul class="folder">
          <li data-id=3>catA_1
          <ul>
            <li data-id=4>catA_1_1</li>
          </ul> 
          <ul>
            <li data-id=5>catA_1_2</li>
          </ul> 
          <ul>
            <li data-id=6>catA_1_3</li>
          </ul>                              
          </li>
        </ul>
        <ul class="folder">
          <li data-id=8>catA_3
            <ul>
              <li data-id=9>catA_3_1</li>
            </ul>
            <ul>
              <li data-id=10>catA_3_2</li>
            </ul>
            <ul class="folder">
              <li data-id=11>catA_3_3
                <ul>
                  <li data-id=12>catA_3_3_1</li>
                </ul>
                <ul data-id=13>catA_3_3_2</ul>
              </li>
            </ul>
          </li>
        </ul>
        <ul>
          <li data-id=7>catA_2</li>  <!-- *Note these two (data-id = 7 & 14) trail the above "folders" as they have no items in them -->
        </ul>
        <ul>
          <li data-id=14>catA_4</li>
        </ul>               
      </li>
    </ul>
    <ul>
      <li data-id=2>catB</li> <!-- And so on with catB -->
    </ul>    
  </li>
</ul>

  const src = [[1,"catA",0],[2,"catB",0],[3,"catA_1",1],[4,"catA_1_1",3],[5,"catA_1_2",3],[6,"catA_1_3",3],[7,"catA_2",1],[8,"catA_3",1],[9,"catA_3_1",8],[10,"catA_3_2",8],[11,"catA_3_3",8],[12,"catA_3_3_1",11],[13,"catA_3_3_2",11],[14,"catA_4",1],[15,"catB_1",2],[16,"catB_1_1",15],[17,"catB_1_2",16],[18,"catB_1_3",16],[19,"catB_2",15],[20,"catB_3",15],[21,"catB_3_1",20],[22,"catB_3_2",20],[23,"catB_3_3",20],[24,"catB_3_3_1",23],[25,"catB_3_3_2",23],[26,"catB_4",15]];

function tree(src, parent = 0) {
  const el = document.getElementById("myList").querySelector("li[data-id='" + parent + "']");
  
  if (!el) return;
  
  for (var i = 0; i < src.length; i++) {
    if (src[i][2] === parent) {
      const new_parent = src[i][0];
      el.insertAdjacentHTML("beforeend", "<ul><li data-id='" + new_parent + "'>" + src[i][1] + "</li></ul>");
      el.parentElement.classList.add("folder");
      tree(src, new_parent);
    }
  }
}

tree(src)
<ul id='myList'>
  <li data-id=0></li>
</ul>

EDIT: To clarify, the commmented li with data-id=7, (catA_2) is currently being placed alphabetically between two ".folder" ul's instead of after the ".folder" ul's


Solution

  • I think you're looking for something like this.

    It first converts the array of arrays to an object of objects. Then it adds each child to its parent item. Then it sorts them so that items with children come before items without children. Then it recursively generates the markup.

    (Note: I'm using different sample data because the data in the original question has some inconsistencies between parent IDs and names).

    If the data is alphabetized, it's semantically more correct to use an ordered list instead of an unordered list because there's an inherent order to the data.

    const src = [[1, "catA", 0], [2, "catA_1", 1], [3, "catA_1_1", 2], [4, "catA_1_2", 2], [5, "catA_2", 1], [7, "catB", 0], [8, "catB_1", 7], [9, "catB_1_1", 8], [10, "catB_2", 7], [11, "catB_2_1", 10], [12, "catA_3", 1], [13, "catA_3_1", 12], [14, "catA_3_2", 12], [15, "catA_3_1_1", 13], [16, "catA_3_1_2", 13], [17, "catA_3_2_1", 14], [18, "catA_3_2_2", 14]]
    
    function buildNestedList(src) {
      
      const nodes = {}
      const rootNodes = []
    
      // build an object of the items, with each item's id as the key
      src.forEach(([id, name, parentId]) => {
        nodes[id] = { id, name, parentId, children: [] }
      })
    
      // build the tree structure
      Object.values(nodes).forEach((node) => {
        if (node.parentId === 0) {
          rootNodes.push(node)
        } else {
          const parent = nodes[node.parentId]
          if (parent) {
            parent.children.push(node)
          } else {
            console.warn(`Parent with ID ${node.parentId} not found for node ID ${node.id}`)
          }
        }
      })
    
      // sort nodes: first alphabetically by name, then prioritize those with children
      function sortNodes(nodes) {
        // sort alphabetically first
        nodes.sort((a, b) => a.name.localeCompare(b.name))
        // then sort by whether they have children (those with children first)
        return nodes.sort((a, b) => {
          if (a.children.length > 0 && b.children.length === 0) {
            return -1
          } else if (a.children.length === 0 && b.children.length > 0) {
            return 1
          }
          return 0
        })
      }
    
      // recursive function to create dom elements
      function createList(nodes) {
        const ol = document.createElement("ol")
        sortNodes(nodes).forEach((node) => {
          const li = document.createElement("li")
          li.textContent = node.name
          li.dataset.id = node.id
          if (node.children.length > 0) {
            li.appendChild(createList(node.children))
          }
          ol.appendChild(li)
        })
        return ol
      }
    
      // generate and return the hierarchy
      return createList(rootNodes)
      
    }
    
    document.body.appendChild(buildNestedList(src))