0

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

18
  • Does your code produce the desired outcome? What is px (referring to the part of the code that calls px.tree(....)? Commented Oct 23, 2024 at 19:12
  • Oops! That px came from my actual code. Edited it out now. No, that function is ordering alphabetically, meaning that catA_2 would be slotted in as a "file" between "folders" Commented Oct 23, 2024 at 19:16
  • No, that function is ordering alphabetically - doesn't seem to be alphabetical order, seems reverse alphabetical order Commented Oct 23, 2024 at 19:18
  • I think you're right. Getting a little lost in providing example code vs actual code. My actual code is ordered in reverse alphabet so that this would result in correct alphabet Commented Oct 23, 2024 at 19:22
  • did you want "beforeend" instead of "afterend" maybe? Commented Oct 23, 2024 at 19:22

1 Answer 1

1

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))

Sign up to request clarification or add additional context in comments.

8 Comments

My bad on the source data inconsistencies! Thanks for this great explanation, it seems like a much neater way of going about things. It is almost perfect but for items number 2 & 3 of catA ol should be reversed as catA_2, having no children, should come after catA_3
@midget I've modified it so that items with children come before items without children.
You are the main dude! It's so close and I should be able to take it from here but it seems each ol is then ordered by data-id instead of textContent alphapetically
Actually it turns out I don't know how to rearrange the li's alphabetically
Yeah, if you add braces you also have to add an explicit return
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.